Dream-HTML – render HTML, SVG, MathML, Htmx markup from OCaml

https://github.com/yawaramin/dream-html

API Reference

dream-html - build robust and maintainable OCaml Dream webapps

Copyright 2023 Yawar Amin

This file is part of dream-html.

dream-html is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

dream-html is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with dream-html. If not, see https://www.gnu.org/licenses/.

What

This library started as a simple HTML library; it has evolved into something more over time. Here are the highlights:

  • Closely integrated with the Dream web framework for OCaml
  • Generate HTML using type-safe functions and values
  • MathML and SVG support
  • Support for htmx attributes
  • Type-safe HTML form and query decoding
  • Type-safe path parameter parsing and printing

Note

If you're not using Dream, you can still use the HTML/SVG/MathML/htmx generation features using the pure-html package.

HTML generation first look

let page req =
  let open Dream_html in
  let open HTML in
  (* automatically injects <!doctype html> *)
  html [lang "en"] [
    head [] [
      title [] "Dream-html";
    ];
    body [] [
      h1 [] [txt "Dream-html"];
      p [] [txt "Is cool!"];
      form [method_ `POST; action "/feedback"] [
        (* Integrated with Dream's CSRF token generation *)
        csrf_tag req;
        label [for_ "what-you-think"] [txt "Tell us what you think!"];
        input [name "what-you-think"; id "what-you-think"];
        input [type_ "submit"; value "Send"];
      ];
    ];
  ]
(* Integrated with Dream response *)
let handler req = Dream_html.respond (page req)

Security (HTML escaping)

Attribute and text values are escaped using rules very similar to standards- compliant web browsers:

utop # open Dream_html;;
utop # open HTML;;
utop # #install_printer pp;;
utop # let user_input = "<script>alert('You have been pwned')</script>";;
val user_input : string = "<script>alert('You have been pwned')</script>"
utop # p [] [txt "%s" user_input];;
- : node = <p>&lt;script&gt;alert('You have been pwned')&lt;/script&gt;</p>
utop # div [title_ {|"%s|} user_input] [];;
- : node = <div title="&quot;<script>alert('You have been pwned')</script>"></div>

How to install

Make sure your local copy of the opam repository is up-to-date first:

opam update
opam install dream-html # or pure-html if you don't want the Dream integration

Alternatively, to install the latest commit that may not have been released yet, you have two options. If you need only the HTML generation:

opam pin add pure-html git+https://github.com/yawaramin/dream-html

If you also need the Dream integration:

opam pin add pure-html git+https://github.com/yawaramin/dream-html
opam pin add dream-html git+https://github.com/yawaramin/dream-html

Usage

A convenience is provided to respond with an HTML node from a handler:

Dream_html.respond greeting

You can compose multiple HTML nodes together into a single node without an extra DOM node, like React fragments:

let view = null [p [] [txt "Hello"]; p [] [txt "World"]]

You can do string interpolation of text nodes using txt and any attribute which takes a string value:

let greet name = p [id "greet-%s" name] [txt "Hello, %s!" name]

You can conditionally render an attribute, and void elements are statically enforced as childless:

let entry =
  input [
    if should_focus then autofocus else null_;
    id "email";
    name "email";
    value "Email address";
  ]

You can also embed HTML comments in the generated document:

div [] [comment "TODO: xyz."; p [] [txt "Hello!"]]
(* <div><!-- TODO: xyz. -->Hello!</div> *)

You have precise control over whitespace in the rendered HTML; dream-html does not insert any whitespace by itself–all whitespace must be inserted inside text nodes explicitly:

p [] [txt "hello, "; txt "world!"];;
(* <p>hello, world!</p> *)

You can also conveniently hot-reload the webapp in the browser using the Dream_html.Livereload module. See the API reference for details.

Form validation

There is also a module with helpers for request form and query validation; see Dream_html.Form for details. See also the convenience helpers Dream_html.form and Dream_html.query.

Type-safe path parameter parsing and printing

Type-safe wrappers for Dream routing functionality are provided; details are shown in the Dream_html page.

Import HTML

One issue that you may come across is that the syntax of HTML is different from the syntax of dream-html markup. To ease this problem, you may use the translation webapp in the landing page.

Note that the dream-html code is not formatted nicely, because the expectation is that you will use ocamlformat to fix the formatting.

Also note that the translation done by this bookmarklet is on a best-effort basis. Many web pages don't strictly conform to the rules of correct HTML markup, so you will likely need to fix those issues for your build to work.

Test

Run the test and print out diff if it fails:

dune test # Will also exit 1 on failure

Set the new version of the output as correct:

Prior art/design notes

Surface design obviously lifted straight from elm-html.

Implementation inspired by both elm-html and ScalaTags.

Many languages and libraries have similar HTML embedded DSLs:

{
"by": "todsacerdoti",
"descendants": 6,
"id": 40217334,
"kids": [
40221922,
40221958
],
"score": 22,
"time": 1714516914,
"title": "Dream-HTML – render HTML, SVG, MathML, Htmx markup from OCaml",
"type": "story",
"url": "https://github.com/yawaramin/dream-html"
}
{
"author": "yawaramin",
"date": null,
"description": "Render HTML, SVG, MathML, htmx markup from your OCaml app - yawaramin/dream-html",
"image": "https://opengraph.githubassets.com/91407d7cda828cf915599d3eac634551f93c1afe2b93bd9070d3bcfeb5aab8ee/yawaramin/dream-html",
"logo": "https://logo.clearbit.com/github.com",
"publisher": "GitHub",
"title": "GitHub - yawaramin/dream-html: Render HTML, SVG, MathML, htmx markup from your OCaml app",
"url": "https://github.com/yawaramin/dream-html"
}
{
"url": "https://github.com/yawaramin/dream-html",
"title": "GitHub - yawaramin/dream-html: Render HTML, SVG, MathML, htmx markup from your OCaml app",
"description": "API Reference dream-html - build robust and maintainable OCaml Dream webapps Copyright 2023 Yawar Amin This file is part of dream-html. dream-html is free software: you can redistribute it and/or modify it...",
"links": [
"https://github.com/yawaramin/dream-html"
],
"image": "https://opengraph.githubassets.com/91407d7cda828cf915599d3eac634551f93c1afe2b93bd9070d3bcfeb5aab8ee/yawaramin/dream-html",
"content": "<div><article><p>\n <a target=\"_blank\" href=\"https://yawaramin.github.io/dream-html/dream-html/Dream_html/\">API Reference</a>\n</p>\n<p></p><h2>dream-html - build robust and maintainable OCaml Dream webapps</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#dream-html---build-robust-and-maintainable-ocaml-dream-webapps\"></a><p></p>\n<p>Copyright 2023 Yawar Amin</p>\n<p>This file is part of dream-html.</p>\n<p>dream-html is free software: you can redistribute it and/or modify it under\nthe terms of the GNU General Public License as published by the Free Software\nFoundation, either version 3 of the License, or (at your option) any later\nversion.</p>\n<p>dream-html is distributed in the hope that it will be useful, but WITHOUT\nANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\nFOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p>\n<p>You should have received a copy of the GNU General Public License along with\ndream-html. If not, see <a target=\"_blank\" href=\"https://www.gnu.org/licenses/\">https://www.gnu.org/licenses/</a>.</p>\n<p></p><h2>What</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#what\"></a><p></p>\n<p>This library started as a simple HTML library; it has evolved into something more\nover time. Here are the highlights:</p>\n<ul>\n<li>Closely integrated with the <a target=\"_blank\" href=\"https://aantron.github.io/dream/\">Dream</a> web\nframework for OCaml</li>\n<li>Generate HTML using type-safe functions and values</li>\n<li>MathML and SVG support</li>\n<li>Support for htmx attributes</li>\n<li>Type-safe HTML form and query decoding</li>\n<li>Type-safe path parameter parsing and printing</li>\n</ul>\n<div><p>Note</p><p>If you're not using Dream, you can still use the HTML/SVG/MathML/htmx\ngeneration features using the <code>pure-html</code> package.</p>\n</div>\n<p></p><h2>HTML generation first look</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#html-generation-first-look\"></a><p></p>\n<div><pre><span>let</span> <span>page</span> <span>req</span> <span>=</span>\n <span>let</span> <span>open</span> <span>Dream_html</span> <span>in</span>\n <span>let</span> <span>open</span> <span>HTML</span> <span>in</span>\n <span><span>(*</span> automatically injects &lt;!doctype html&gt; <span>*)</span></span>\n html [lang <span><span>\"</span>en<span>\"</span></span>] [\n head [] [\n title [] <span><span>\"</span>Dream-html<span>\"</span></span>;\n ];\n body [] [\n h1 [] [txt <span><span>\"</span>Dream-html<span>\"</span></span>];\n p [] [txt <span><span>\"</span>Is cool!<span>\"</span></span>];\n form [method_ <span>`POST</span>; action <span><span>\"</span>/feedback<span>\"</span></span>] [\n <span><span>(*</span> Integrated with Dream's CSRF token generation <span>*)</span></span>\n csrf_tag req;\n label [for_ <span><span>\"</span>what-you-think<span>\"</span></span>] [txt <span><span>\"</span>Tell us what you think!<span>\"</span></span>];\n input [name <span><span>\"</span>what-you-think<span>\"</span></span>; id <span><span>\"</span>what-you-think<span>\"</span></span>];\n input [type_ <span><span>\"</span>submit<span>\"</span></span>; value <span><span>\"</span>Send<span>\"</span></span>];\n ];\n ];\n ]\n<span><span>(*</span> Integrated with Dream response <span>*)</span></span>\n<span>let</span> <span>handler</span> <span>req</span> <span>=</span> <span>Dream_html.</span>respond (page req)</pre></div>\n<p></p><h2>Security (HTML escaping)</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#security-html-escaping\"></a><p></p>\n<p>Attribute and text values are escaped using rules very similar to standards-\ncompliant web browsers:</p>\n<div><pre><code>utop # open Dream_html;;\nutop # open HTML;;\nutop # #install_printer pp;;\nutop # let user_input = \"&lt;script&gt;alert('You have been pwned')&lt;/script&gt;\";;\nval user_input : string = \"&lt;script&gt;alert('You have been pwned')&lt;/script&gt;\"\nutop # p [] [txt \"%s\" user_input];;\n- : node = &lt;p&gt;&amp;lt;script&amp;gt;alert('You have been pwned')&amp;lt;/script&amp;gt;&lt;/p&gt;\nutop # div [title_ {|\"%s|} user_input] [];;\n- : node = &lt;div title=\"&amp;quot;&lt;script&gt;alert('You have been pwned')&lt;/script&gt;\"&gt;&lt;/div&gt;\n</code></pre></div>\n<p></p><h2>How to install</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#how-to-install\"></a><p></p>\n<p>Make sure your local copy of the opam repository is up-to-date first:</p>\n<div><pre><code>opam update\nopam install dream-html # or pure-html if you don't want the Dream integration\n</code></pre></div>\n<p>Alternatively, to install the latest commit that may not have been released yet,\nyou have two options. If you need <em>only</em> the HTML generation:</p>\n<div><pre><code>opam pin add pure-html git+https://github.com/yawaramin/dream-html\n</code></pre></div>\n<p>If you <em>also</em> need the Dream integration:</p>\n<div><pre><code>opam pin add pure-html git+https://github.com/yawaramin/dream-html\nopam pin add dream-html git+https://github.com/yawaramin/dream-html\n</code></pre></div>\n<p></p><h2>Usage</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#usage\"></a><p></p>\n<p>A convenience is provided to respond with an HTML node from a handler:</p>\n<div><pre><span>Dream_html.</span>respond greeting</pre></div>\n<p>You can compose multiple HTML nodes together into a single node without an extra\nDOM node, like <a target=\"_blank\" href=\"https://react.dev/reference/react/Fragment\">React fragments</a>:</p>\n<div><pre><span>let</span> view <span>=</span> null [p [] [txt <span><span>\"</span>Hello<span>\"</span></span>]; p [] [txt <span><span>\"</span>World<span>\"</span></span>]]</pre></div>\n<p>You can do string interpolation of text nodes using <code>txt</code> and any attribute which\ntakes a string value:</p>\n<div><pre><span>let</span> <span>greet</span> <span>name</span> <span>=</span> p [id <span><span>\"</span>greet-%s<span>\"</span></span> name] [txt <span><span>\"</span>Hello, %s!<span>\"</span></span> name]</pre></div>\n<p>You can conditionally render an attribute, and\n<a target=\"_blank\" href=\"https://developer.mozilla.org/en-US/docs/Glossary/Void_element\">void elements</a>\nare statically enforced as childless:</p>\n<div><pre><span>let</span> entry <span>=</span>\n input [\n <span>if</span> should_focus <span>then</span> autofocus <span>else</span> null_;\n id <span><span>\"</span>email<span>\"</span></span>;\n name <span><span>\"</span>email<span>\"</span></span>;\n value <span><span>\"</span>Email address<span>\"</span></span>;\n ]</pre></div>\n<p>You can also embed HTML comments in the generated document:</p>\n<div><pre>div <span>[]</span> [comment <span><span>\"</span>TODO: xyz.<span>\"</span></span>; p [] [txt <span><span>\"</span>Hello!<span>\"</span></span>]]\n<span><span>(*</span> &lt;div&gt;&lt;!-- TODO: xyz. --&gt;Hello!&lt;/div&gt; <span>*)</span></span></pre></div>\n<p>You have precise control over whitespace in the rendered HTML; dream-html does\nnot insert any whitespace by itself–all whitespace must be inserted inside text\nnodes explicitly:</p>\n<div><pre>p <span>[]</span> [txt <span><span>\"</span>hello, <span>\"</span></span>; txt <span><span>\"</span>world!<span>\"</span></span>];;\n<span><span>(*</span> &lt;p&gt;hello, world!&lt;/p&gt; <span>*)</span></span></pre></div>\n<p>You can also conveniently hot-reload the webapp in the browser using the\n<code>Dream_html.Livereload</code> module. See the API reference for details.</p>\n<p></p><h2>Form validation</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#form-validation\"></a><p></p>\n<p>There is also a module with helpers for request form and query validation; see\n<a target=\"_blank\" href=\"https://yawaramin.github.io/dream-html/dream-html/Dream_html/Form/index.html\"><code>Dream_html.Form</code></a>\nfor details. See also the convenience helpers <code>Dream_html.form</code> and\n<code>Dream_html.query</code>.</p>\n<p></p><h2>Type-safe path parameter parsing and printing</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#type-safe-path-parameter-parsing-and-printing\"></a><p></p>\n<p>Type-safe wrappers for Dream routing functionality are provided; details are\nshown in the\n<a target=\"_blank\" href=\"https://yawaramin.github.io/dream-html/dream-html/Dream_html/#type-safe-routing\"><code>Dream_html</code></a> page.</p>\n<p></p><h2>Import HTML</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#import-html\"></a><p></p>\n<p>One issue that you may come across is that the syntax of HTML is different from\nthe syntax of dream-html markup. To ease this problem, you may use the\ntranslation webapp in the <a target=\"_blank\" href=\"https://yawaramin.github.io/dream-html/\">landing page</a>.</p>\n<p>Note that the dream-html code is not formatted nicely, because the expectation is\nthat you will use ocamlformat to fix the formatting.</p>\n<p>Also note that the translation done by this bookmarklet is on a best-effort\nbasis. Many web pages don't strictly conform to the rules of correct HTML\nmarkup, so you will likely need to fix those issues for your build to work.</p>\n<p></p><h2>Test</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#test\"></a><p></p>\n<p>Run the test and print out diff if it fails:</p>\n<div><pre><code>dune test # Will also exit 1 on failure\n</code></pre></div>\n<p>Set the new version of the output as correct:</p>\n<p></p><h2>Prior art/design notes</h2><a target=\"_blank\" href=\"https://github.com/yawaramin/dream-html#prior-artdesign-notes\"></a><p></p>\n<p>Surface design obviously lifted straight from\n<a target=\"_blank\" href=\"https://package.elm-lang.org/packages/elm/html/latest/\">elm-html</a>.</p>\n<p>Implementation inspired by both elm-html and\n<a target=\"_blank\" href=\"https://com-lihaoyi.github.io/scalatags/\">ScalaTags</a>.</p>\n<p>Many languages and libraries have similar HTML embedded DSLs:</p>\n<ul>\n<li><a target=\"_blank\" href=\"https://www.phlex.fun/\">Phlex</a> - Ruby</li>\n<li><a target=\"_blank\" href=\"https://activeadmin.github.io/arbre/\">Arbre</a> - Ruby</li>\n<li><a target=\"_blank\" href=\"https://github.com/garlic0x1/hiccl\">hiccl</a> - Common Lisp</li>\n<li><a target=\"_blank\" href=\"https://docs.racket-lang.org/scribble-pp/html-html.html\">scribble-html-lib</a> -\nRacket</li>\n<li><a target=\"_blank\" href=\"https://github.com/weavejester/hiccup\">hiccup</a> - Clojure</li>\n<li><a target=\"_blank\" href=\"https://nim-lang.org/docs/htmlgen.html\">std/htmlgen</a> - Nim</li>\n<li><a target=\"_blank\" href=\"https://github.com/pimbrouwers/Falco.Markup\">Falco.Markup</a> - F#</li>\n<li><a target=\"_blank\" href=\"https://htpy.dev/\">htpy</a> - Python</li>\n<li><a target=\"_blank\" href=\"https://metacpan.org/pod/HTML::Tiny\">HTML::Tiny</a> - Perl</li>\n<li><a target=\"_blank\" href=\"https://j2html.com/\">j2html</a> - Java</li>\n<li><a target=\"_blank\" href=\"https://github.com/chrisdone/lucid\">Lucid</a> - Haskell</li>\n</ul>\n</article></div>",
"author": "",
"favicon": "https://github.githubassets.com/favicons/favicon.svg",
"source": "github.com",
"published": "",
"ttr": 175,
"type": "object"
}