<!DOCTYPE html>
<html lang="en">

<head>
  <title>dream-html - Render HTML, SVG, MathML, htmx markup from your OCaml Dream backend server</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover">

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">

  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/ocaml.min.js"></script>
  <script async src="https://buttons.github.io/buttons.js"></script>

  <script>
    const dreamHtmlSnippet = (() => {
      let txt = '';

      return {
        get() {
          return txt;
        },
        set(newTxt) {
          txt = newTxt;
        },
      };
    })();
  </script>
</head>

<body class="container">
  <header>
    <nav>
      <ul>
        <li>
          <strong>dream-html</strong>
        </li>
      </ul>
      <ul>
        <li><a href="https://yawaramin.github.io/dream-html/dream-html/Dream_html/">Dream-html API docs</a></li>
        <li><a href="https://yawaramin.github.io/dream-html/pure-html/Pure_html/">Pure-html API docs</a></li>
        <li><a class="github-button" href="https://github.com/yawaramin/dream-html"
            data-color-scheme="no-preference: light; light: light; dark: dark;" data-size="large" data-show-count="true"
            aria-label="Star yawaramin/dream-html on GitHub">Star</a></li>
      </ul>
    </nav>
  </header>
  <main>
    <section>
      <div class="grid">
        <script>
          const cp = async () => {
            await navigator.clipboard.writeText(dreamHtmlSnippet.get());
          };
        </script>
        <div class="overflow-auto">
          <p>Renders OCaml: <button onclick="cp()" class="secondary" style="padding: 0 4px 0 4px">Copy</button></p>
          <pre><code class="language-ocaml">open Dream_html
open HTML</code></pre>
          <pre><code class="language-ocaml" id="dream-html-snippet">html [lang "en"] [
  head [] [
    title [] "My page";
  ];
  body [] [
    h1 [] [txt "My text"];
  ];
]</code></pre>
        </div>
        <div>
          <p>To HTML/SVG/MathML (try it):</p>
          <textarea id="html-snippet" style="height: 350px; font-family: monospace"><!DOCTYPE html>
<html lang="en">
  <head>
    <title>My page</title>
  </head>
  <body>
    <h1>My text</h1>
  </body>
</html></textarea>
          <script>
            (() => {
              const parser = new DOMParser();

              const suffixAttrs = {
                'cite': 1, 'class': 1, 'data': 1, 'for': 1, 'form': 1, 'label': 1,
                'method': 1, 'object': 1, 'open': 1, 'slot': 1, 'span': 1, 'style': 1,
                'title': 1, 'type': 1,
              };
              const ariaAttrs = {
                'aria-activedescendant': 1, 'aria-atomic': 1, 'aria-autocomplete': 1,
                'aria-braillelabel': 1, 'aria-brailleroledescription': 1, 'aria-busy': 1,
                'aria-checked': 1, 'aria-colcount': 1, 'aria-colindextext': 1,
                'aria-colspan': 1, 'aria-controls': 1, 'aria-current': 1,
                'aria-describedby': 1, 'aria-description': 1, 'aria-details': 1,
                'aria-disabled': 1, 'aria-errormessage': 1, 'aria-expanded': 1,
                'aria-flowto': 1, 'aria-haspopup': 1, 'aria-hidden': 1, 'aria-invalid': 1,
                'aria-keyshortcuts': 1, 'aria-label': 1, 'aria-labelledby': 1,
                'aria-level': 1, 'aria-live': 1, 'aria-modal': 1, 'aria-multiline': 1,
                'aria-multiselectable': 1, 'aria-orientation': 1, 'aria-owns': 1,
                'aria-placeholder': 1, 'aria-posinset': 1, 'aria-pressed': 1,
                'aria-readonly': 1, 'aria-relevant': 1, 'aria-required': 1,
                'aria-roledescription': 1, 'aria-rowcount': 1, 'aria-rowindex': 1,
                'aria-rowindextext': 1, 'aria-rowspan': 1, 'aria-selected': 1,
                'aria-setsize': 1, 'aria-sort': 1, 'aria-valuemax': 1, 'aria-valuemin': 1,
                'aria-valuenow': 1, 'aria-valuetext': 1,
              };
              const hxAttrs = {
                'hx-boost': 1, 'hx-confirm': 1, 'hx-delete': 1, 'hx-disable': 1,
                'hx-disinherit': 1, 'hx-encoding': 1, 'hx-ext': 1, 'hx-get': 1,
                'hx-headers': 1, 'hx-history': 1, 'hx-history-elt': 1, 'hx-include': 1,
                'hx-indicator': 1, 'hx-on': 1, 'hx-params': 1, 'hx-patch': 1, 'hx-post': 1,
                'hx-preload': 1, 'hx-preserve': 1, 'hx-prompt': 1, 'hx-push-url': 1,
                'hx-put': 1, 'hx-replace-url': 1, 'hx-request': 1, 'hx-select': 1,
                'hx-select-oob': 1, 'hx-sse-connect': 1, 'hx-sse-swap': 1, 'hx-swap': 1,
                'hx-swap-oob': 1, 'hx-sync': 1, 'hx-target': 1, 'hx-trigger': 1,
                'hx-validate': 1, 'hx-vals': 1, 'hx-ws-connect': 1, 'hx-ws-send': 1,
              };
              const polyVarAttrs = {
                'autocapitalize': 1, 'autocomplete': 1, 'capture': 1, 'crossorigin': 1,
                'decoding': 1, 'dir': 1, 'enctype': 1, 'fetchpriority': 1, 'formenctype': 1,
                'formmethod': 1, 'hidden': 1, 'http_equiv': 1, 'inputmode': 1, 'kind': 1,
                'low': 1, 'method': 1, 'preload': 1, 'referrerpolicy': 1, 'role': 1,
                'translate': 1, 'wrap': 1,
                'aria-autocomplete': 1, 'aria-checked': 1, 'aria-current': 1,
                'aria-haspopup': 1, 'aria-invalid': 1, 'aria-live': 1, 'aria-orientation': 1,
                'aria-pressed': 1, 'aria-relevant': 1, 'aria-sort': 1,
              };
              const numAttrs = {
                'cols': 1, 'colspan': 1, 'high': 1, 'low': 1, 'maxlength': 1, 'minlength': 1,
                'optimum': 1, 'rows': 1, 'rowspan': 1, 'span': 1, 'start': 1, 'tabindex': 1,
                'aria-valuemax': 1, 'aria-valuemin': 1, 'aria-valuenow': 1,
              };
              const boolAttrs = {
                'async': 1, 'autofocus': 1, 'autoplay': 1, 'checked': 1, 'controls': 1,
                'default': 1, 'defer': 1, 'disabled': 1, 'draggable': 1, 'formnovalidate': 1,
                'ismap': 1, 'loop': 1, 'multiple': 1, 'muted': 1, 'novalidate': 1, 'open': 1,
                'playsinline': 1, 'readonly': 1, 'required': 1, 'reversed': 1, 'selected': 1,
                'aria-atomic': 1, 'aria-busy': 1, 'aria-disabled': 1, 'aria-modal': 1,
                'aria-multiline': 1, 'aria-multiselectable': 1, 'aria-readonly': 1,
                'aria-required': 1,
                'hx-disable': 1, 'data-hx-disable': 1, 'hx-history-elt': 1,
                'data-hx-history-elt': 1, 'hx-preload': 1, 'data-hx-preload': 1,
                'hx-preserve': 1, 'data-hx-preserve': 1, 'hx-validate': 1,
                'data-hx-validate': 1, 'hx-ws-send': 1, 'data-hx-ws-send': 1,
              };
              const voidTags = {
                'area': 1, 'base': 1, 'br': 1, 'col': 1, 'embed': 1, 'hr': 1, 'img': 1,
                'input': 1, 'link': 1, 'meta': 1, 'source': 1, 'track': 1, 'wbr': 1,
              };
              const textTags = {
                'option': 1, 'script': 1, 'style': 1, 'textarea': 1, 'title': 1
              };

              const htmlTags = {
                ...voidTags,
                ...textTags,
                'a': 1, 'abbr': 1, 'address': 1, 'article': 1, 'aside': 1,
                'audio': 1, 'b': 1, 'bdi': 1, 'bdo': 1, 'blockquote': 1, 'body': 1,
                'button': 1, 'canvas': 1, 'caption': 1, 'cite': 1, 'code': 1,
                'colgroup': 1, 'data': 1, 'datalist': 1, 'dd': 1, 'del': 1, 'details': 1,
                'dfn': 1, 'dialog': 1, 'div': 1, 'dl': 1, 'dt': 1, 'em': 1,
                'fieldset': 1, 'figcaption': 1, 'figure': 1, 'footer': 1, 'form': 1, 'h1': 1,
                'h2': 1, 'h3': 1, 'h4': 1, 'h5': 1, 'h6': 1, 'head': 1, 'header': 1, 'hgroup': 1,
                'html': 1, 'i': 1, 'iframe': 1, 'ins': 1, 'kbd': 1,
                'label': 1, 'legend': 1, 'li': 1, 'main': 1, 'map': 1, 'mark': 1, 'menu': 1,
                'meter': 1, 'nav': 1, 'noscript': 1, 'object': 1, 'ol': 1, 'optgroup': 1,
                'output': 1, 'p': 1, 'param': 1, 'picture': 1, 'pre': 1, 'progress': 1,
                'q': 1, 'rp': 1, 'rt': 1, 'ruby': 1, 's': 1, 'samp': 1, 'section': 1,
                'select': 1, 'small': 1, 'span': 1, 'strong': 1, 'sub': 1,
                'summary': 1, 'sup': 1, 'table': 1, 'tbody': 1, 'td': 1, 'template': 1,
                'tfoot': 1, 'th': 1, 'thead': 1, 'time': 1, 'tr': 1, 'u': 1,
                'ul': 1, 'var': 1, 'video': 1
              };

              const attr = name => {
                if (suffixAttrs[name]) return name + '_';
                if (ariaAttrs[name]) return 'Aria.' + name.slice(5);
                if (hxAttrs[name]) return 'Hx.' + name.slice(3).replaceAll('-', '_');

                if (
                  name.length >= 9 &&
                  name.startsWith('data-hx-') &&
                  hxAttrs[name.slice(5)] > -1
                ) return 'Hx.' + name.slice(8).replaceAll('-', '_');

                if (name.indexOf('-') > -1) return 'string_attr "' + name + '"';
                return name;
              };

              const polyvar = v => {
                let v2 = v;

                switch (v2) {
                  case 'get':
                  case 'post':
                    v2 = v2.toUpperCase();
                    break;
                  case 'true':
                  case 'false':
                    v2 += '_';
                    break;
                  default:
                    v2 = v2.replaceAll('-', '_');
                }

                return '`' + v2;
              };

              const stringify = (v, nm = '') => {
                if (v == null) return '';
                if (polyVarAttrs[nm]) return polyvar(v);
                if (numAttrs[nm]) return v;
                if (boolAttrs[nm]) return '';
                if (v.indexOf('"') > -1) return '{|' + v + '|}';
                return '"' + v + '"';
              };

              document.getElementById('html-snippet').addEventListener('input', evt => {
                let inHead = false;
                let res = '';

                const writeTag = t => {
                  switch (t.nodeType) {
                    case Node.COMMENT_NODE:
                      res += 'comment ';
                      res += stringify(t.data);
                      break;

                    case Node.TEXT_NODE:
                      if (inHead) {
                        res += 'comment "**"';
                        break;
                      }

                      res += 'txt ';
                      res += stringify(t.data);
                      break;

                    case Node.ELEMENT_NODE:
                      const name = t.tagName.toLowerCase();

                      if (name == 'head') {
                        inHead = true;
                      } else if (name == 'body') {
                        inHead = false;
                      }

                      if (htmlTags[name]) {
                        res += name;
                      } else {
                        res += 'std_tag ' + stringify(name);
                      }

                      res += ' [';
                      for (const a of t.attributes) {
                        res += attr(a.name);
                        res += ' ';
                        res += stringify(a.value, a.name);
                        res += '; ';
                      }
                      res += '] ';

                      if (voidTags[name]) {
                        /* do nothing */
                      } else if (textTags[name]) {
                        res += stringify(t.innerText);
                      } else {
                        res += '[';
                        for (const child of t.childNodes) {
                          res += '\n';
                          writeTag(child);
                          res += ';';
                        }
                        res += ']';
                      }
                      break;

                    default:
                      console.warn(t.nodeType);
                  }
                };

                writeTag(parser.parseFromString(evt.target.value, 'text/html').getElementsByTagName('html')[0]);
                dreamHtmlSnippet.set(res);

                const { value: markup } = hljs.highlight(res, { language: 'ocaml' });
                document.getElementById('dream-html-snippet').innerHTML = markup;
              })
            })()
          </script>
        </div>
      </div>
    </section>
    <section>
      <h2>Install & use</h2>

      <p>See the <a href="https://github.com/yawaramin/dream-html">readme</a>.</p>
    </section>
  </main>
  <script>hljs.highlightAll();</script>
</body>

</html>