/hypertag

HTML templates with Python-like concise syntax, code reuse & modularity. The Pythonic way to web templating.

Primary LanguagePythonMIT LicenseMIT

Introduction

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.

Quick Start

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

Blocks

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: &amp; &lt; &gt;
    </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 &amp; 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.

Tags

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>

Expressions

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

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

Filters

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

Control blocks

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.

Built-ins

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

Further reading

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.

Cheat Sheet

Text blocks

 
Syntax
 
Description
 
| text plain-text block; may contain embedded expressions; output is HTML-escaped
/ markup markup block; may contain embedded expressions; output is not HTML-escaped
! verbatim verbatim block; expressions are not parsed; output is not HTML-escaped
| multi-line
  text.....
text blocks may span multiple lines, also when preceded by a tag; subsequent lines must be indented
-- comment
# comment
line of comment; is excluded from output; may occur at the end of a block's headline (inline comment) or on a separate line (block comment)
< BLOCK dedent modifier (<): when put on the 1st line of a BLOCK, causes the output to be dedented by one level during rendering; applies to blocks of all types (text, control etc.)
... BLOCK append modifier (...): when put on the 1st line of a BLOCK, causes the output to be appended to the previous block without a newline

Expressions

 
Syntax
 
Description
 
$x = a-b assignment block; space after $ is allowed
$x
$x.v[1]
embedding of a factor expression (a variable with 0+ tail operators) in a text block or string
{x+y} embedding of an arbitrary expression in a text block or string
x!
{x*y}!
"obligatory" qualifier (!) for an atomic or embedded expression; raises as exception if the expression is false
x?
{x*y}?
"optional" qualifier (?) for an atomic or embedded expression; replaces exceptions and false values with empty strings
'text' f-string (formatted string), may contain embedded expressions: $... and {...}
"text" f-string (formatted string), may contain embedded expressions: $... and {...}
r'text' r-string (raw string), expressions are left unparsed
r"text" r-string (raw string), expressions are left unparsed
$$ {{ }} escape strings; render $ or { or } in a plaintext/markup block and inside formatted strings

Tags

 
Syntax
 
Description
 
div
     p | text
tagged block starts with a tag name (header) that can be followed by contents (body) on the same line (inline body) and/or on subsequent lines (outline body)
p | this is
     a multiline
     paragraph
a tagged block with exclusively text contents may span multiple lines
box: | Title
    li | item1
    li | item2
mixing inline and outline contents is possible when the colon (:) and a text-block marker (|/!) are both present
h1 : b : a href='' :
     | text
tags can be chained together using colon (:); trailing colon is optional
box "top" x=1.5
a href=$url
a href={url}
unnamed and named (keyword) attributes can be passed to a tag in a space-separated list, no parentheses; expressions can be used as values
%SUM x y=0 | sum is {x+y}
%TAB @cells n:
     | Table no. $n
     @cells
hypertag definition block (%) may declare attributes, possibly with defaults, and may have a body; the "at" sign (@) marks a special attribute that will hold actual body of hypertag's occurrence: without this attribute the hypertag is void (must have empty contents in places of occurrence)
@body
@body[2:]
embedding block (@): inserts DOM nodes represented by an expression (typically a body attribute inside hypertag definition)
div .CLASS (shortcut) equiv. to class="CLASS" on attributes list of a tag
div #ID (shortcut) equiv. to id="ID" on attributes list of a tag
pass "empty block" placeholder; generates no output, does not accept attributes nor a body
.
. | text
the special null tag (.) outputs its body without changes; helps improve vertical alignment of text in adjecent blocks; does not accept attributes