HypertextTemplates.jl
Hypertext templating DSL for Julia
This package provides a collection of macros for building and rendering HTML from Julia code using all the normal control flow syntax of the language, such as loops and conditional. No intermediate "virtual" DOM is constructed during rendering process, which reduces memory allocations. Supports streaming renders of templates via a StreamingRender
iterator.
When rendered in "development" mode source locations of all elements within the rendered within the DOM are preserved, which allows for lookup from the browser to open the file and line that generated a specific DOM element. This source information is stripped out during production usage to minimise transferred data and avoid leaking internal server details to clients. Ctrl+1
will jump to the @render
that created the fragment of HTML under the cursor. Ctrl+2
will jump to the specific element macro call site that generated the fragment of HTML under the cursor.
DSL Basics
using HypertextTemplates
using HypertextTemplates.Elements
@render @div {class = "bg-blue-400"} begin
@h1 "Document title"
@p "Content goes here."
@ul for num in 1:3
@li {id = num} @text num
end
end
The DSL (domain specific language) is made up of macro calls that represent HTML elements that match their official names (with a prefixed @
). Element attributes are written with {}
surrounding key/value pairs defined using =
. Nesting of elements within other elements is done using beign end
blocks, or for simple cases just as a single expression argument to the macro.
The @render
macro wraps the elements and converts it to a String
value. If you want to render to a predefined IO
object then pass that object as the first argument to @render
, eg. @render my_buffer @div ...
.
Normal looping and conditional syntax is valid within element macros. So the for
syntax above results in a ul
list with three li
children with content 1
, 2
, and 3
respectively. This extends to any third-party packages that provide their own control flow macros, such as pattern matching.
The @text
macro is used when the argument to an element macro is not a simple string literal and marks the expression for rendering into the output.
Custom Elements
The HypertextTemplates.Elements
module exports all valid HTML element names and so should cover most usage. If you want to render a custom element name then use the @element
macro to define it.
@element "my-element" my_element
The first argument defines the HTML tag to render. The second is the Julia identifier to use within code to reference the element definition.
The my_element
definition can then be used within a DOM definition with the @<
macro:
@render @<my_element {class = "rounded-xl"}
If the @<
macro syntax is too cumbersome for the intended usage of the custom element then the @deftag
macro can be used to define a macro equivalent to my_element
:
@deftag macro my_element end
@render @my_element {id = 1} begin
@p "content"
end
@component
Element definitions can be split up into parts for ease of reuse and maintainability by using the @component
macro.
@component function my_component(; prop = "default", n = 1)
@ul {class = prop} for i in 1:n
@li {id = i} "content"
end
end
@deftag macro my_component end
@render @div begin
@my_component
@my_component {prop = "custom", n = 2}
end
Note how a @deftag
was also defined for my_component
such that it could be invoked with the macro syntax rather than with @<my_component
syntax.
The keywords defined for the function are the equivalent of "properties" that you might fine in other component systems within frontend development technologies. They operate in the exact same way as normal Julia keywords.
@<
The @<
macro that was previously introduced allows for using components and elements as first class values; similar to how we can pass Function
objects to other functions in Julia.
@component function my_component(; elem = p, class = "default")
@div begin
@<elem {class}
end
end
@render @div begin
@my_component
@my_component {elem = strong, class = "custom"}
end
@__once__
When you need to render HTML to a page only once per page, for example a JS dependency that only needs including once via <script>
, you can use this macro to do that. It ensures that during a single @render
call the contents of each @__once__
are only evaluated once even if the rendered components are called more that once.
Most common use cases are for including @link
, @style
, or @script
tags.
@component function jquery()
@__once__ begin
@script {src = "https://code.jquery.com/jquery-3.6.0.min.js"}
end
end
@deftag macro jquery end
@component function jq_button()
@jquery
@button "Click Me"
end
@deftag macro jq_button end
@component function page()
@html begin
@head begin
@jquery
end
@body begin
@h1 "Hello, World!"
@jq_button
end
end
end
@deftag macro page end
Property Names
Typically property names, which are defined between {}
s are written as Julia identifiers. If one of your property names needs to be an invalid word, perhaps containing a -
character then you can also use string literals, ""
, to define the property name. To avoid Julia's linter complaining about string literals on the left hand side of =
s you can replace them with :=
, which is equivalent.
@render @div {"x-data" := "{ open: false }"} begin
@button {"@click" := "open = true"} "Expand"
@span {"x-show" := "open"} "Content..."
end
The {}
property syntax works very similarly to NamedTuple
syntax, so if a property has the same name as a variable you want to use as its value then just using the variable itself is allow. ...
syntax is also supported to splat a collection of key/value pairs into {}
s.
var = "variable"
props = (; prop = "value", other = true)
@render @div {var, props...}
Note that true
values are rendered just as their property name, while false
values are not printed at all. If you need to specifically render other="true"
in the DOM then write @div {other="true"}
instead.
@cm_component
Markdown files can be turned into component definitions that behave the same way as normal @component
s. This requires the CommonMark.jl
package to be available in your project's dependencies.
@cm_component markdown_file(; prop) = "file.md"
The same set of extensions supported by the @cm_str
macro in CommonMark.jl
are supported in markdown components, expression interpolation included. This means that any keyword props provided in the component definition, such as prop
above can be interpolated into the markdown file and will be rendered into the final HTML output that the component generates.
StreamingRender
A StreamingRender
is an iterator that handles asynchronous execution of @render
calls. This is useful if your @component
potentially takes a long time to render completely and you wish to begin streaming the HTML to the browser as it becomes available.
for bytes in StreamingRender(io -> @render io @slow_component {args...})
write(http_stream, bytes)
end
do
-block syntax is also, naturally, supported by the StreamingRender
constructor. All @component
definitions support streaming out-of-the-box. Be aware that rendering happens in a Threads.@spawn
ed task.
Docstrings
HypertextTemplates.SafeString
— TypeSafeString(str)
A string type that bypasses the default HTML escaping that is applied to all strings rendered with @render
and DOM element properties. Only apply this type to string values that are not user-defined. If applying this to user-defined values ensure that proper sanitization has been carried out.
HypertextTemplates.StreamingRender
— TypeStreamingRender(func)
An iterable that will run the render function func
, which takes a single io
argument that must be passed to the @render
macro call.
for bytes in StreamingRender(io -> @render io @component {args...})
write(http_stream, bytes)
end
Or use a do
block rather than ->
syntax.
HypertextTemplates.TemplateFileLookup
— MethodTemplateFileLookup(handler)
This is a developer tool that can be added to an HTTP
handler stack to allow the user to open the template file in their default editor by holding down the Ctrl
key and clicking on the rendered template. This is useful for debugging navigating the template files instead of having to manually search through a codebase for the template file that renders a given item within a page.
HTTP.serve(router |> TemplateFileLookup, host, port)
Always add the TemplateFileLookup
handler after the other handlers, since it needs to inject a script into the response to listen for clicks on the rendered template.
HypertextTemplates.render
— Methodrender([dst], component; properties...)
Render the component
with the given properties
to the optional dst
. This is the functional version of @render
.
HypertextTemplates.@<
— Macro@<TAG
@<TAG children...
@<TAG {props...}
@<TAG {props...} children...
Render the TAG
component or element with the given children and props.
HypertextTemplates.@__once__
— Macro@__once__ expr
Evaluate expr
only once per @render
call.
HypertextTemplates.@__slot__
— Macro@__slot__ [name]
Mark a slot location within the DOM. An unnamed slot will run all expressions that are not named within a @<
expression, while named slots will only run the expressions that are named with name := expr
syntax.
HypertextTemplates.@cm_component
— Macro@cm_component component_name(; props...) = "file_name.md"
Creates a new @component
definition from a markdown file. The CommonMark.jl
package is used to parsed and render the contents of the file hence that package must be installed as a dependency, since this features is provided via the package extension mechanism.
Just as with a regular @component
you can provide props
to a markdown component that will be used in any interpolated values (using the CommonMark
$
syntax for interpolation).
When Revise.jl
is active and tracking the package which contains a @cm_component
definition updates to the source markdown file are tracked and passed to Revise
to perform code revision.
HypertextTemplates.@component
— Macro@component function component_name(; properties...)
# ...
end
Define a new reusable component definition.
HypertextTemplates.@deftag
— Macro@deftag macro name end
@deftag name
Create a macro version of the given element or component called name
. This is a shorthand way to call the @<
macro with the given name
while still correctly passing the source information through to the renderer. The macro
variant allows the LSP to correctly infer the location of the definition whereas the plain symbol variant does not.
HypertextTemplates.@element
— Macro@element name [html_name]
Define an HTML element name
that prints to HTML as html_name
, which defaults to name
itself.
HypertextTemplates.@esc_str
— Macroesc""
Perform HTML string escaping at macro expansion time rather than runtime.
HypertextTemplates.@render
— Macro@render [destination] dom
Renders the dom
tree to the destination
object. When no destination
is provided then the dom
is rendered directly to String
and returned as the value of the expression.
This is only needed for rendering the root of the DOM tree, not for the output of each individual component that is defined.
HypertextTemplates.@text
— Macro@text content...
Embed the given content
as plain text in the surrounding DOM tree. Can only be used within DOM expressions. Text is HTML-escaped before printing to the output. Use SafeString
to mark text as "safe" so that it is not escaped.