Page
Library
Module
Module type
Parameter
Class
Class type
Source
Dream_htmlSourcedream-html is a library of useful functionality needed for making robust and maintainable server-driven web applications using the Dream web framework.
Typed, extensible HTML form decoder with error reporting for form field validation failures. Powerful chained decoding functionality–the validation of one field can depend on the values of other decoded fields.
val form :
'a Form.t ->
?csrf:bool ->
Dream.request ->
[> 'a Dream.form_result | `Invalid of (string * string) list ] Dream.promiseType-safe wrapper for Dream.form. Similarly to that, you can match on the result:
type new_user = { name : string; email : string }
let new_user =
let open Dream_html.Form in
let+ name = required string "name"
and+ email = required string "email" in
{ name; email }
(* POST /users *)
let post_users req =
match%lwt Dream_html.form new_user req with
| `Ok { name; email } -> (* ... *)
| `Invalid errors -> Dream.json ~code:422 ( (* ...render errors... *) )
| _ -> Dream.empty `Bad_RequestType-safe wrapper for Dream.all_queries. Can be used to decode the query parameters into a typed value.
Correct construction of markup with the help of type-safe helpers and combinators.
include module type of Pure_htmlThese are the types of the final values which get rendered.
E.g. id="toast".
Either a tag, a comment, or text data in the markup.
Same as to_string but render void tags as XML-style self-closing tags.
Same as pp but render void tags as XML-style self-closing tags.
Special handling for string-value attributes so they can use format strings i.e. string interpolation.
A 'void element': https://developer.mozilla.org/en-US/docs/Glossary/Void_element with no children.
Tags which can have attributes but can contain only text. The text can be formatted.
attr name is a new attribute which does not carry any payload. E.g.
let required = attr "required"string_attr name fmt is a new string-valued attribute which allows formatting i.e. string interpolation of the value. Note, the fmt argument is required due to the value restriction.
Convenience for attributes whose values should be URIs. Takes care of both URI-encoding and attribute escaping, as recommended in https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#common-mistake.
Examples
a [href "/blog?tags=iamsafe\"></a><script>alert('Pwned')</script>"] [txt "Tags: tag1 | tag2"]
==>
<a href="/blog?tags=iamsafe%22%3E%3C/a%3E%3Cscript%3Ealert('Pwned')%3C/script%3E">Tags: tag1 | tag2</a>
a [href "/foo?a=1&b=2 3&c=4<5&d=6>5"] [txt "Test"]
==>
<a href="/foo?a=1&b=2%203&c=4%3C5&d=6%3E5">Test</a>Build a tag which can contain only a URI. The URI is escaped with the same rules as a uri_attr.
A text node inside the DOM e.g. the 'hi' in <b>hi</b>. Allows string interpolation using the same formatting features as Printf.sprintf:
b [] [txt "Hello, %s!" name]Or without interpolation:
b [] [txt "Bold of you."]HTML-escapes the text value. You can use the ~raw param to bypass escaping:
let user_input = "<script>alert('I like HTML injection')</script>" in
txt ~raw:true "%s" user_inputA comment that will be embedded in the rendered HTML, i.e. <!-- comment -->. The text is HTML-escaped.
concat node list is the list of nodes joined together into a single node, with each element separated by node.
Add an attribute to a tag.
let toast msg = p [id "toast"] [txt "%s" msg]
let toast_oob = toast "ok." +@ Hx.swap_oob "true"If adding the class attribute to a tag which already has a class attribute, join together the values of the CSS classes:
p [class_ "foo"] [] +@ class_ "bar"Result:
<p class="foo bar"></p>Get the value of an existing attribute.
let toast = p [id "toast"] [txt "OK."]
let toast_id = toast.@["id"]Get whether a node is null (empty) or not. Useful for conditional rendering of UIs when you are passed in a node and you don't know if it's empty or not.
val fold :
tag:(string -> attr list -> 'a -> 'a) ->
txt:(string -> 'a -> 'a) ->
comment:(string -> 'a -> 'a) ->
'a ->
node ->
'afold ~tag ~txt ~comment value node is the value resulting from 'folding over' the node with an initial value and calling the following callbacks for each child in the node's tree:
Eg calculate a list of all the classes used by a node and its children:
let tag _name attrs classes =
match List.assoc_opt "class" attrs with
| Some c -> c :: classes
| None -> classes
and txt_or_comment _string classes = classes in
fold ~tag ~txt:txt_or_comment ~comment:txt_or_comment [] nodeAll standard HTML attributes and tags. Some attributes and tags have the same name, e.g. style. To disambiguate them, attributes have a _ (underscore) suffix.
Convenience wrappers that use dream-html attributes and nodes instead of raw strings.
val respond :
?status:[< Dream.status ] ->
?code:int ->
?headers:(string * string) list ->
node ->
Dream.response Dream.promiseval redirect :
?status:[< Dream.redirection ] ->
?code:int ->
?headers:(string * string) list ->
?flash:string ->
Dream.request ->
attr ->
Dream.response Dream.promiseredirect ?status ?code ?headers ?flash req href is the same as Dream.redirect but instead of taking a string location to redirect to, it takes an attribute (you'll usually want to use href). The reason for taking an attribute is that we can construct correct route paths in attributes without having to hard-code the entire path. Eg,
let%path order = "/orders/%s"
...
let order_id = ... in
Dream_html.redirect req (path_attr href order order_id)val send :
?text_or_binary:[< Dream.text_or_binary ] ->
?end_of_message:[< Dream.end_of_message ] ->
Dream.websocket ->
node ->
unit Dream.promiseType-safe wrapper for Dream.send.
Type-safe wrapper for Dream.set_body. Sets the body to the given node and sets the Content-Type header to text/html.
Type-safe wrapper for Dream.write.
Convenience to add a CSRF token generated by Dream into your form. Type-safe equivalent of Dream.csrf_tag.
form
[action "/foo"]
[csrf_tag req; input [name "bar"]; input [type_ "submit"]]Helpers for managing HTTP conditional requests. Note that the ETag values derived from the key parameters are not using cryptographically secure hashing. For example, for a key foo, we currently derive an ETag of "acbd18db4cc2f85cedef654fccc4a4d8". It is your responsibility to provide a key that uniquely identifies the version of the resource and changes when the resource is updated.
Eg, the last modified date of a file is a reasonable key for the contents of the file because it is guaranteed to change when the file is changed. Of course, you may prefer other even better ways to generate keys for resources.
See MDN documentation for examples of idiomatic usage of these headers.
val if_none_match :
Dream.request ->
key:[< `None | `Strong of string | `Weak of string ] ->
(unit -> Dream.response Dream.promise) ->
Dream.response Dream.promiseif_none_match req ~key refresh checks the If-None-Match header of req to see if it contains an ETag derived from the key. If so, it responds with 304 Not Modified and an empty body. Otherwise, it re-fetches the resource corresponding to key using refresh (), and sets the ETag in the response header.
val if_match :
Dream.request ->
key:[< `None | `Strong of string | `Weak of string ] ->
(unit -> Dream.response Dream.promise) ->
Dream.response Dream.promiseif_match req ~key save checks if the If-Match header of req matches the ETag derived from the key. If so, it calls save () and returns the response. Otherwise, it responds with an error 412 Precondition Failed.
Bidirectional paths with type-safe path segment parsing and printing using OCaml's built-in format strings, and fully plug-and-play compatible with Dream routes.
A path that can be used for routing and can also be printed as an attribute value.
Wrapper for a Dream route that represents the ability to parse path parameters and pass them to the handler function with the correct types.
val path :
('r, unit, Dream.response Dream.promise) format ->
('p, unit, string, attr) format4 ->
('r, 'p) pathpath request_fmt attr_fmt is a router path. The dream-html.ppx provides a more convenient way.
Without PPX: let order = path "/orders/%s" "/orders/%s"
With PPX: let%path order = "/orders/%s"
Refer to the PPX documentation for instructions on using it.
⚠️ Due to the way Dream's router works, all parameter captures happen between / characters and the end of the path (or the ? character, whichever comes first). Eg, /foo/%s/bar/%d is valid, but /foo/%s.%s (note the dot character) is not a valid capture.
⚠️ If a route is matched but the data type does not match, a 400 Bad Request response will be returned. The following type conversion specs are supported:
%s capture a string and pass it to the handler
%*s capture the rest of the path and pass the captured length and string to the handler
%c capture a char
%d or %i capture an int
%x capture a hexadecimal int
%X capture an uppercase hexadecimal int
%o capture an octal int
%ld capture an int32
%Ld capture an int64
%f capture a float
%B capture a bool
⚠️ We are actually using Dream's built-in router, not our own, and Dream's router doesn't distinguish between parameter types. So, to Dream both /%s and /%d are the same path. It will route the request to whichever happens to be first in the route list, and that one will succeed or fail depending on its type and the request target.
path_attr attr path is an HTML attribute with the path parameters filled in from the given values. Eg,
let%path order = "/orders/%s"
open Dream_html
open HTML
a [path_attr href order "yzxyzc"] [txt "My Order"]Renders: <a href="/orders/yzxyzc">My Order</a>
Use this instead of hard-coding your route URLs throughout your app, to make it easy to refactor routes with minimal effort.
pp_path is a pretty-printer for path values. For a path like path "/foo" "/foo", it will print out /foo.
Type-safe wrappers for Dream.get and so on. Using the PPX, eg:
let%path order = "/orders/%s"
let get_order = get order (fun request order_id ->
...
a [path_attr href order order_id] [txt "Your order"]
...
)use middlewares routes is a route that is composed of all the given routes with the middlewares attached to them.
static_asset path is a route that handles serving the static file at the path. Importantly, it sets an immutable cache header which remains valid for a year.
⚠️ Be sure that the resource has a unique identifier because it will be cached immutably. The dreamwork CLI tool automates this for you. See Dreamwork.
TL;DR:
dreamwork setupstatic/assets/ subdirectory with filesdune buildStatic.routes to the app's main Dream routerimg [path_attr src Static.Assets.icon_png] which will render with a revision hash based on the file contents, calculated at build timedreamwork is a CLI tool that helps set up and manage static file paths and routes with proper content-based version hashes. The static files will live inside a dune component called static and in the static/assets subdirectory. Suppose you have the following directory tree:
static/
dune
assets/
css/
app.css
js/
app.jsThe dune file defines a library component that will make the following module available:
module Static : sig
val routes : Dream.route
(** This route will serve all of the following paths. *)
module Assets : sig
module Css : sig
val app_css : (Dream.response Dream.promise, attr) path
end
module Js : sig
val app_js : (Dream.response Dream.promise, attr) path
end
end
endSo, you can just stick Static.routes in your router and it will correctly serve the files from the filesystem with an immutable cache of 1 year; and you can use Static.Assets.Css.app_css and so on in your dream-html markup code and it will correctly render with a ?rev=... query parameter that uniquely identifies this revision of the file with a content-based hash for cache-busting purposes:
link [rel "stylesheet"; path_attr href Static.Assets.Css.app_css
(*
<link
rel="stylesheet"
href="/static/assets/css/app.css?rev=17fb8161afc85df86156ea1f3744c8a2"
>
*) script [path_attr src Static.Assets.Js.app_js] ""
(*
<script src="/static/assets/js/app.js?rev=677645e5ac37d683c5039a85c41c339f">
</script>
*)You control the directory subtree under assets; the dreamwork CLI just helps you define the dune component that generates the above module structure. The module structure mirrors the directory tree structure.
The entry point to dreamwork is the dreamwork setup command, which creates static/, assets/, and dune. In the dune file it defines a code generation rule which uses the dreamwork static command to generate the OCaml code.
So, you just need to run dreamwork setup to initialize the directory structure and code generation. After that, you can add and remove any files inside assets/ as you want and on the next dune build the Static module structure will be updated accordingly.
Live reload script injection and handling. Adapted from Dream.livereload middleware. This version is not a middleware so it's not as plug-and-play as that, but on the other hand it's much simpler to implement because it uses type-safe dream-html nodes rather than parsing and printing raw HTML. See below for the 3-step process to use it.