Source code of Typ2HTML

Also available directly on the repo.

# justfile
compile-raw SRC TGT:
  typst compile --features html --format html --root . {{SRC}} {{TGT}}

watch-raw SRC TGT:
  typst watch --features html --format html --root . --no-serve {{SRC}} {{TGT}}

compile BASE:
  just compile-raw _src/pages/{{BASE}}.typ {{BASE}}.html

watch BASE:
  just watch-raw _src/pages/{{BASE}}.typ {{BASE}}.html

compile-all:
  ls _src/pages | cut -d. -f1 | parallel --lb just compile {}

watch-all:
  ls _src/pages | cut -d. -f1 | parallel --lb just watch {}

/* _assets/global.css */
/* Color definitions, gruvbox style */
:root {
  --black: #1d2021;
  --dk-gray0: #282828;
  --dk-gray1: #3c3836;
  --dk-gray2: #504945;
  --dk-gray3: #665c54;
  --dk-gray4: #7c6f64;
  --gray: #928374;
  --white: #fbf1c7;
  --lt-gray0: #ebdbb2;
  --lt-gray1: #d5c4a1;
  --lt-gray2: #bdae93;
  --lt-gray3: #a89984;
  --dk-red: #cc241d;
  --dk-green: #98971a;
  --dk-yellow: #d79921;
  --dk-blue: #458588;
  --dk-purple: #b16286;
  --dk-aqua: #689d6a;
  --dk-orange: #d65d0e;
  --lt-red: #fb4934;
  --lt-green: #b8bb26;
  --lt-yellow: #fabd2f;
  --lt-blue: #83a598;
  --lt-purple: #d3869b;
  --lt-aqua: #8ec07c;
  --lt-orange: #fe8019;
}

/* {main: */
html {
  background-color: var(--black);
}
body {
  margin: 40px auto;
  /* TODO: get something working if on mobile */
  max-width: 1200px;
  line-height: 1.5;
  font-size: 18px;
  font-weight: 350;
  color: var(--white);
  background: var(--dk-gray0);
  padding: 1cm 5mm 10cm 5mm;
}

h1,h2,h3,h4 {
  line-weight: 1.2;
  color: var(--lt-green);
}
h1 { font-size: 33px }
h2 { font-size: 31px }
h3 { font-size: 29px }
h4 { font-size: 27px }

a { color: var(--dk-aqua) }
a:visited { color: var(--lt-purple) }
/* :main} */
/* _src/t2h/mod.typ */
#import "css.typ"
#import "html.typ"
#import "struct.typ"
#import "excerpt.typ"
/* _src/t2h/html.typ */
// {func:
#let htmlfunc(label) = (..attrs) => {
  let keyvals = attrs.named().pairs().filter(p => p.at(1) != none).to-dict()
  html.elem(label, attrs: keyvals, ..attrs.pos())
}
// :func}

// {apply:
#let div = htmlfunc("div")
#let a = htmlfunc("a")
#let img = htmlfunc("img")
// :apply}

#let h1 = htmlfunc("h1")
#let h2 = htmlfunc("h2")
#let h3 = htmlfunc("h3")
#let p = htmlfunc("p")
#let span = htmlfunc("span")
#let link = htmlfunc("link")
#let pre = htmlfunc("pre")
#let style = htmlfunc("style")
#let script = htmlfunc("script")
#let code = htmlfunc("code")
#let footer = htmlfunc("footer")

/* _src/t2h/css.typ */
#import "html.typ"

// TODO: document and test more thoroughly

#let aux-to-css = (
  pt: (abs) => str(abs) + "pt",
  em: (em) => str(em) + "em"
)

// TODO: find more types that are useful
// - fr
#let type-to-css = (
  string: (elt) => elt,
  integer: (elt) => str(elt),
  float: (elt) => str(elt),
  "auto": (_) => "auto",
  length: (len) => {
    let abs = len.abs.pt()
    let em = len.em
    if em == 0 {
      (aux-to-css.pt)(abs)
    } else if abs == 0 {
      (aux-to-css.em)(em)
    } else {
      "calc(" + (aux-to-css.em)(em) + " + " + (aux-to-css.pt)(abs) + ")"
    }
  },
  ratio: (pct) => repr(pct),
)

#let into-css-str(elt) = {
  let has-trivial-impl = type-to-css.at(str(type(elt)), default: none)
  if has-trivial-impl != none {
    has-trivial-impl(elt)
  } else if type(elt) == relative {
    let len = elt.length
    let abs = len.abs.pt()
    let em = len.em
    let rat = elt.ratio
    let (sign-abs, abs) = if abs < 0 {
      (" - ", -abs)
    } else {
      (" + ", abs)
    }
    let (sign-em, em) = if em < 0 {
      (" - ", -em)
    } else {
      (" + ", em)
    }
    "calc(" + (type-to-css.ratio)(rat) + sign-em + (aux-to-css.em)(em) + sign-abs + (aux-to-css.pt)(abs) + ")"
  } else if type(elt) == color {
    elt.to-hex()
  } else if type(elt) == stroke {
    let thickness = elt.thickness
    if thickness == auto { thickness = 1pt }
    let paint = elt.paint
    if paint == auto { paint = black }
    into-css-str((thickness, "solid", paint))
  } else if type(elt) == array {
    elt.map(into-css-str).join(" ")
  } else {
    panic("Unsupported type " + str(type(elt)))
  }
}

// Generate raw CSS not bound to a class.
// This can be put in a `style=...` parameter.
// The input should have the form of a dictionary of (key, value) pairs
// so that every `key: str(value)` is a valid CSS setting.
#let raw-style(params) = {
  let ans = params.pairs().map(args => {
    let (key, val) = args
    key + ": " + into-css-str(val) + ";"
  }).join(" ")
  if ans == none { "" } else { ans }
}

// Bind a style to a class.
#let raw-elem(target, params) = {
  target + " { " + raw-style(params) + " }"
}

// Interpret a dictionary as multiple instances of `raw-elem`.
#let raw-elems(dict) = {
  dict.pairs().map(args => {
    let (key, val) = args
    raw-elem(key, val)
  }).join("\n")
}

// Include the raw CSS for `raw-elem` in a <style> tag.
#let elem(target, params) = {
  html.style(raw-elem(target, params))
}

// Include the raw CSS for `raw-elems` in a <style> element.
#let elems(dict) = {
  html.style(raw-elems(dict))
}

// Include a separately written CSS file into the document.
#let include-file(path) = {
  html.link(rel: "stylesheet", href: path)
}
/* _src/t2h/struct.typ */
#import "html.typ"
#import "css.typ"
#import "once.typ"

#let builtin-stroke = stroke

// Easy way to optionally pass `class: class` to a function.
#let class-key(class) = {
  if class == none { (:) } else {
    (class: class)
  }
}

// Implement alignment, simulating Typst's `align`.
#let align(where, inner) = {
  let style = (
    width: 100%,
    display: "flex",
    flex-wrap: "wrap",
    flex-direction: "row",
  )
  // Vertical handling.
  if where.y == top {
    style.margin-bottom = "auto"
  } else if where.y == bottom {
    style.margin-top = "auto"
  }
  // Horizontal handling.
  if where.x == right {
    style.text-align = "right"
    style.justify-content = "flex-end"
  } else if where.x == left {
    style.text-align = "left"
    style.justify-content = "flex-start"
  } else {
    style.text-align = "center"
    style.justify-content = "center"
  }
  // Make the div.
  html.div(style: css.raw-style(style), {
    inner
  })
}

// Some of the constructors that follow provide one of two behaviors:
// - create the element right now; or
// - create a builder that lazily declares a CSS style and binds it to the element.
// The first is easier, but might result in a larger HTML file.
//
// - `base-style` should be the default style
// - `style-func` takes named parameters and optionally generates more style
// - `elem-func` takes
//    - optionally, a class name to fetch the defaults from
//    - optionally, more style to override the defaults
//    - the inner contents
#let structural(base-style, style-func, elem-func) = {
  // Resulting function is capable of returning either the element
  // or a builder for the element
  let func(inline: true, class: none, ..args) = {
    // Build the default style from all named arguments
    let style = style-func(base-style, ..args.named())
    if inline {
      // In inline mode, build the element directly
      elem-func(..class-key(class), style: css.raw-style(style), ..args.pos())
    } else {
      // In non-inline mode, there must be a class specified.
      assert(class != none)
      // We return a builder that will on-demand...
      let html-elem-builder(..extra-args) = {
        // ...declare the default style at most once
        once.once("style:" + class, css.elem("." + class, style))
        // ...bind the defaults and any overriding values to the specific element
        let extra-style = style-func((:), ..extra-args.named())
        elem-func(..class-key(class), style: css.raw-style(extra-style), ..extra-args.pos())
      }
      html-elem-builder
    }
  }
  func
}

// `text` simulates Typst's function of the same name.
#let text-base = (:)
#let text-style(
  base,
  size: none, fill: none,
  // TODO: font
) = {
  let style = base
  if size != none { style.font-size = size }
  if fill != none { style.color = fill }
  style
}
#let text-elem(class: none, style: none, inner) = {
  html.span(..class-key(class), style: style, inner)
}
#let text = structural(text-base, text-style, text-elem)


// `box` simulates Typst's function of the same name.
// TODO: comment
#let box-base = (
  display: "inline-flex", flex-direction: "line",
  justify-content: "center", align-items: "center",
  flex-wrap: "wrap",
)
#let box-style(
  base,
  inset: none, width: none, height: none, radius: none,
  fill: none, outset: none, stroke: none,
  // TODO: stroke
) = {
  let style = base
  if width != none { style.width = width }
  if height != none { style.height = height }
  if radius != none {
    if type(radius) == dictionary {
      let precedence = (
        ("top-left", ("top-left",)),
        ("top-right", ("top-right",)),
        ("bottom-left", ("bottom-left",)),
        ("bottom-right", ("bottom-right",)),
        ("top", ("top-left", "top-right")),
        ("bottom", ("bottom-left", "bottom-right")),
        ("left", ("top-left", "bottom-left")),
        ("right", ("top-right", "bottom-right")),
        ("rest", ("top-left", "top-right", "bottom-left", "bottom-right")),
      )
      let radii = (:)
      for (key, targets) in precedence {
        if key in radius {
          for target in targets {
            if target not in radii {
              radii.insert(target, radius.at(key))
            }
          }
        }
      }
      style.border-radius = (radii.at("top-left", default: 0), radii.at("top-right", default: 0),
        radii.at("bottom-right", default: 0), radii.at("bottom-left", default: 0)).map(css.into-css-str).join(" ")
    } else {
      style.border-radius = radius
    }
  }
  if fill != none { style.background-color = fill }
  if inset != none {
    if type(inset) == dictionary {
      if "y" in inset {
        style.padding-top = inset.y
        style.padding-bottom = inset.y
      }
      if "x" in inset {
        style.padding-left = inset.x
        style.padding-right = inset.x
      }

    } else {
      style.padding = inset
    }
  }
  if outset != none {
    if type(outset) == dictionary {
      if "y" in outset {
        style.margin-top = outset.y
        style.margin-bottom = outset.y
      }
      if "x" in outset {
        style.margin-left = outset.x
        style.margin-right = outset.x
      }
    } else {
      style.margin = outset
    }
  }
  if stroke != none {
    if type(stroke) == dictionary {
      if "top" in stroke or "bottom" in stroke or "left" in stroke or "right" in stroke or "rest" in stroke {
        for key in ("top", "bottom", "left", "right") {
          if key in stroke {
            style.insert("border-" + key, builtin-stroke(stroke.at(key)))
          }
        }
        if "rest" in stroke {
          style.border = builtin-stroke(stroke.rest)
        }
      } else {
        style.border = builtin-stroke(stroke)
      }
    } else {
      style.border = builtin-stroke(stroke)
    }
  }
  style
}
#let box-elem(class: none, style: none, inner) = {
  html.div(..class-key(class), style: style, inner)
}
#let box = structural(box-base, box-style, box-elem)

// `table` simulates Typst's function of the same name.
#let table-base = (display: "grid", )
#let table-style(
  base,
  class: none, columns: none, gutter: none, column-gutter: none,
  row-gutter: none,
  // TODO: stroke
  // TODO: more options for column width control.
) = {
  let style = base
  if columns != none { style.grid-template-columns = "repeat(" + str(columns) + ", 1fr)" }
  if gutter != none { style.gap = gutter }
  if column-gutter != none { style.column-gap = column-gutter }
  if row-gutter != none { style.row-gap = row-gutter }
  style
}
#let table-elem(class: none, style: none, inner) = {
  html.div(..class-key(class), style: style, inner)
}
#let table = structural(table-base, table-style, table-elem)

#let box-linebreak = {
  html.div(style: css.raw-style((flex-basis: 100%, height: 0)))
}

/* _src/t2h/excerpt.typ */
#import "html.typ"

#let code(text, lang: "typst") = {
  html.pre({
    html.code(class: "language-" + lang, {
      text
    })
  })
}

#let inline(text, lang: "typst") = {
  html.span({
    html.code(class: "language-" + lang, {
      text
    })
  })
}

// Based on the language, choose what style of comments to use.
#let comment-style = (
  typst: (t) => "/* " + t + " */",
  css: (t) => "/* " + t + " */",
  make: (t) => "# " + t,
  markdown: (t) => "<!-- " + t + " -->",
  "none": (t) => "# " + t,
)

// This function allows one to insert pieces of source code in the final document.
// This is useful for tutorials because it guarantees that the version
// of the code that is shown is definitionally the latest version.
//
// To use, put in your source code the tags
//   {snippet-name:
//   ...
//   :snippet-name}
// before and after the region you want to copy,
// and invoke `excerpt.incl("path/to/file", "snippet-name")`
//
// The tags work anywhere on the line, so you can wrap them in any comment
// format that is appropriate for the filetype.
//
// If you want the entire file, use instead the companion function `excerpt.full("path/to/file")`
//
// You can optionally specify a language with `lang: ...` (Typst by default).
#let incl(src, label, lang: "typst") = {
  let lines = read("/" + src).split("\n")
  // Find the begin and end tags
  let start = none
  let end = none
  for (idx, line) in lines.enumerate() {
    if line.contains("{" + label + ":") {
      if start == none {
        start = idx
      } else {
        panic("Found the starting tag twice: at l. " + str(start + 1) + ", then at l. " + str(idx + 1))
      }
    }
    if line.contains(":" + label + "}") {
      if end == none {
        end = idx
      } else {
        panic("Found the ending tag twice: at l. " + str(end + 1) + ", then at l. " + str(idx + 1))
      }
    }
  }
  if start == none {
    panic("Did not find the starting tag {" + label + ":")
  }
  if end == none {
    panic("Did not find the ending tag :" + label + "}")
  }
  if start == end {
    panic("The start and and tags are on the same line " + str(idx + 1))
  }
  // TODO: allow disabling the header.
  let fstline = if start + 2 == end {
    comment-style.at(lang)(src + " @ l. " + str(end))
  } else {
    comment-style.at(lang)(src + " @ ll. " + str(start + 2) + "-" + str(end))
  }
  // Construct the block.
  // The parameter `class: "language-xyz"` allows it to be targeted by highlight.js
  code(lang: lang, fstline + "\n" + lines.slice(start + 1, end).join("\n"))
}

#let full(src, lang: "typst") = {
  let lines = read("/" + src)
  // TODO: allow disabling the header.
  let fstline = comment-style.at(lang)(src)
  code(lang: lang, fstline + "\n" + lines)
}
/* _src/common.typ */
#import "/_src/t2h/mod.typ": html, css

// {style:
// Concretely this expands to <link rel="stylesheet" href="_assets/global.css">
#css.include-file("_assets/global.css")
// :style}

// {highlight:
// Unpacked the archive from https://highlightjs.org/download to _highlight/
#html.script(src: "_highlight/highlight.min.js")
#css.include-file("_highlight/styles/base16/gruvbox-dark-soft.css")

// Copied from https://www.npmjs.com/package/@myriaddreamin/highlighter-typst
// the "cjs, js bundled, wasm bundled" script
#html.script(src: "_highlight/highlight-typst.js")

// As soon as the scripts have loaded, highlight all code blocks.
#html.script("
  const run = window.$typst$parserModule.then(() => {
    hljs.registerLanguage('typst', window.hljsTypst({}))
    hljs.highlightAll();
  });
")
// :highlight}

/* _src/footer.typ */
#import "/_src/t2h/mod.typ": css, html, struct

#import struct: *

// Define and style the footer.

#css.elems((
  footer: (
    // Follows the bottom of the page
    position: "fixed",
    bottom: 0,
    // Same width as body, see `_assets/global.css`
    width: 100%,
    max-width: "1200px",
  )
))

#html.footer[
  #box(width: "100%", inset: 1pt, radius: 3pt, fill: "var(--dk-gray2)")[
    #box(inset: (x: 5mm),
      text(size: 10pt)[
        Last build: #datetime.today().display()
      ]
    )
    #box(inset: (x: 5mm),
      text(size: 10pt)[
        Written by Vanille-N using Typst #sys.version
      ]
    )
    #box(inset: (x: 5mm),
      text(size: 10pt)[
        #link("https://github.com/Vanille-N/website/tree/master/data/typ2html/_src")[
          `github:vanille-n/website`
        ]
      ]
    )
  ]
]
/* _src/pages/index.typ */
#let this = "_src/pages/index.typ"
#let common = "_src/common.typ"

// These markers are used to paste pieces of code in the final document.
// See `excerpt.typ`.
// {setup:
#set document(title: "Typ2HTML")
// :setup}

// {prelude:
#import "/_src/t2h/mod.typ": html, css, struct, excerpt
// :prelude}

#include "/_src/common.typ"

= Typst to HTML: playground, tutorial, and showcase

This page, and more broadly all of #link("../index.html")[my website],
are (for the most part) just composed of static HTML and CSS. \
What's interesting is that said HTML is fully generated via
#link("https://typst.app")[Typst]'s experimental
#link("https://typst.app/docs/reference/html/")[HTML export] feature.

In the process of building this website, I found that although experimental,
the feature is already very powerful (though not fully ergonomic yet),
and a bigger obstacle to my development was simply knowing what to do
with the few features available.
This document should make it clear that the issue with Typst's HTML output
currently is not what is *possible*, but what is *convenient*.

This page is intended:
- for current me, a playground where I can test in real time some features;
- for future me, a record of what currently works,
  so that I can see if an update of Typst breaks anything;
- for everyone, a tutorial.
  Not one of what functions are available, mind you
  because that would be #link("https://typst.app/docs/reference/html/elem/")[trivial],
  but rather of how to leverage this one function to obtain nontrivial results.

Via some #link("meta.html#_src/t2h/excerpt.typ")[dark magic], I shall embed in this document
not just the output but also the source code that generates it,
helping towards the goal of this being a usable showcase and tutorial.

You can find the full source code of this page #link("meta.html#_src/pages/index.typ")[here].

// {title:
== Getting started
// :title}

Some stuff just works out of the box.
- setting the title of the page
  #excerpt.incl(this, "setup")
- Putting text in
  // {basic-style:
  *bold*, _italics_, `inline code`
  // :basic-style}
  #excerpt.incl(this, "basic-style")
- Making titles
  #excerpt.incl(this, "title")
- Adding
  // {builtin-link:
  #link("https://example.com")[hyperrefs]
  // :builtin-link}
  #excerpt.incl(this, "builtin-link")
- Bullet lists
  // TODO: highlighting bug here
  // {bullet-list:
  - just
  - like
  - normally

  // :bullet-list}
  #excerpt.incl(this, "bullet-list")
- Enumerations
  // {enum-list:
  1. work
  2. as
  3. well

  // :enum-list}
  #excerpt.incl(this, "enum-list")

== Build process

Before we get into more technical stuff, a small note on my build process.
- `_src/t2h/` contains `html.typ`, `css.typ`, i.e. files that are very generic
  and I expect to be easily reusable;
- `_src/pages/` contains `index.typ`, and I have my `justfile` set to compile
  every `_src/pages/{XYZ}.typ` into `{XYZ}.html`;
- `_highlight/` contains files relevant to syntax highlighting;
- `_assets/` contains additional auxiliary files.
#excerpt.full("justfile", lang: "make")

In other words:
- `just compile index` to compile `index.html` (this file);
- `just watch-all` to dynamically autorecompile the entire directory.

== Non-builtins

// TODO: make all raw display properly

For now Typst only provides the function `elem`,
where to build for example a \
#excerpt.inline(lang: "html", "<div class=\"test\">inner</div>"), you write \
#excerpt.inline("#html.elem(\"div\", attrs: (class: \"test\"), { [inner] })").

I find that it is slighly more convenient to have the following in
#link("meta.html#_src/t2h/html.typ")[`html.typ`]:
#excerpt.incl("_src/t2h/html.typ", "func")
and then instanciate it for each element as such:
#excerpt.incl("_src/t2h/html.typ", "apply")

This means that after importing `"/_src/t2h/mod.typ": html`,
for the same `<div class="test">inner</div>`, we can now write
`html.div(class: "test", { [inner] })`

This document imports `html.typ`,
in which more functions of the same shape are defined.
Thus from now on you can assume that whenever you see
a function `html.foo`, it creates a `<foo>...</foo>` html element.

As a concrete example, here is an image that is also a hyperref:

#excerpt.incl(this, "link")
// {link:
#html.a(href: "https://www.example.com", {
  html.img(src: "_assets/link.svg", width: "50px")
})
// :link}

== Prettification

Now we get to more advanced styling options.
There are at least 4 standard ways of doing this in HTML:
- A static CSS file can be imported into a document with a
  `<link rel="stylesheet" ...>` declaration;
- Raw CSS code can be inserted into a regular `.html` file inside a
  `<style>...</style>` block;
- HTML elements support inline styling attributes as `style=...`;
- a `<script>` block can dynamically set some styling options.

All of these methods can be replicated in Typst.

=== Static global

You can import a pre-written CSS that dictates the style of headers
and the color palette that you see in this document as such:
#excerpt.incl(common, "style")
#excerpt.incl("_assets/global.css", "main", lang: "css")
(see #link("meta.html#_assets/global.css")[`global.css`] if you're curious where
the colors are defined)

=== On-the-fly global

HTML is not limited to a single global `<style>` declaration,
so this can be used also to set more properties after the fact.
For example let us define `my-style` as such:
#excerpt.incl(this, "my-style")

// {my-style:
#let my-style = (
  color: "var(--black)",
  background: "var(--dk-blue)",
  border-radius: 3pt,
  display: "inline-block",
  padding: 5pt,
)
// :my-style}

Then we can for example bind it to the `.on-the-fly` class by:
#excerpt.incl(this, "my-style-fly")

// {my-style-fly:
#css.elem(".on-the-fly", my-style)
#html.div(class: "on-the-fly", {
  [Black on blue (on the fly)]
})
// :my-style-fly}

=== Inline style

Additionally, HTML elements accept a `style` parameter in which you can put CSS.
#excerpt.incl(this, "my-style-inline")

// {my-style-inline:
#html.div(class: "inlined", style: css.raw-style(my-style), {
  [Black on blue (inlined)]
})
// :my-style-inline}

=== Dynamic 

As for much more complex styling, you can always resort to calling external JS code.
In this document, this is the method I use to provide syntax highlighting for code
snippets through #link("https://highlightjs.org/")[`highlight.js`]:

#excerpt.incl(common, "highlight")

== Document layout

Here I offer some very common functions that help set the layout.
I've found that often the appearance is easy to set as just raw CSS,
but the layout (centered / horizontal / vertical / grid / ...) is cumbersome.
Here are functions that should help!
See #link("_src/t2h/struct.typ")[`struct.typ`] for the definition of these functions.
The goal is that these functions should provide as close an interface to the
real Typst version as possible.

#excerpt.incl(this, "import-struct")

// {import-struct:
#import struct: text, box, table, align
// :import-struct}

=== Text

#{let cc(shade, t) = text(fill: "var(--dk-" + shade + ")", t)
[
  #cc("red")[You can] #cc("purple")[control] #cc("blue")[the style] #cc("aqua")[of text]
  #cc("green")[just like] #cc("yellow")[you would] #cc("orange")[in Typst:]
]
}
#excerpt.incl(this, "todo-text")

// {todo-text:
#text(fill: "var(--dk-red)", size: 50pt)[*Some text*]
// :todo-text}

(reminder: you can do color definitions #link("meta.html#_assets/global.css")[like this])

=== Box

`struct.box` tries to mimic to some extent the behavior of Typst's `box`.
It supports automatic or fixed width and height, rounded corners, background color.
#excerpt.incl(this, "styled-box")

// {styled-box:
#box(width: 60%, inset: 10pt, outset: 2mm, radius: 10pt, fill: "var(--dk-purple)", {
  text(fill: "var(--black)")[
    A box \
    with round corners
  ]
})
// :styled-box}

You can even specify corners, margins, and stroke as dictionaries the same way you would
do for a regular Typst `box`.
#excerpt.incl(this, "rounded-corners")

// {rounded-corners:
#box(fill: "var(--dk-gray3)",
  radius: (bottom-left: 1cm, bottom: 3mm),
  stroke: 3pt,
)[
  #box(fill: "var(--dk-aqua)",
    inset: (x: 5mm, y: 2mm), outset: (x: 1cm, y: 2mm),
    radius: (top-left: 5mm, bottom-right: 0, rest: 2mm),
    stroke: (top: green, left: 3pt, right: (paint: red, thickness: 4pt)),
  )[Inner]
]
// :rounded-corners}

=== Table

Partially mimicking Typst's `table` function, we have `struct.table`.
Note that this version does not have a stroke, it's only a grid layout.

#excerpt.incl(this, "table")

// {table:
#let cell = box(fill: "var(--dk-gray1)", height: "100px")[A cell]
#table(
  columns: 3, gutter: "15px", {
    for elt in range(7).map(_ => cell) { elt }
  },
)
// :table}

=== Side note: style builders

If you look at the generated HTML for the code above, you will see that the
inline style for `cell` is repeated as many times as there are cells.
Here I propose a mechanic to cut down on this repetition.

All functions defined in `struct.typ` offer another behavior when passed
the parameter `inline: false`.
Whereas `box(...)` will construct a box, `box(inline: false, class: "box-name", ...)`
will instead return a `box-builder` function that will lazily declare the required
CSS *at most once*.

#excerpt.incl(this, "builder-demo")
// {builder-demo:
#let orange-box = box(
  inline: false, class: "orange-box",
  fill: "var(--dk-orange)", radius: 5pt, width: "fit-content", outset: 5pt,
)
#orange-box[These]
#orange-box[boxes]
#orange-box[share]
#orange-box[the]
#orange-box[same]
#orange-box[style.]
// :builder-demo}
and now the corresponding CSS is not duplicated, resulting in a smaller HTML output!
In fact, the CSS style is included only if necessary, and at most once.

You can still overwrite on-the-fly some elements:
#excerpt.incl(this, "builder-overwrite")
// {builder-overwrite:
#orange-box(inset: 10pt)[Add margins to existing style.]
#orange-box(radius: 0pt)[Remove the rouded corners.]
// :builder-overwrite}

=== Alignment

This mimics Typst's `align` function.
#excerpt.incl(this, "alignment")

// {alignment:
#let gray-box = box(
  inline: false, class: "cell",
  fill: "var(--dk-gray1)", height: 3cm, outset: 3pt, inset: 5pt,
)
#let my-aligned-box(alignment, inner) = {
  gray-box({
    align(alignment)[#inner]
  })
}
#table(columns: 3, gutter: 3mm, {
  my-aligned-box(top + left)[Top left]
  my-aligned-box(top)[Top]
  my-aligned-box(top + right)[Top right]
  my-aligned-box(left)[Left]
  my-aligned-box(center)[Center]
  my-aligned-box(right)[Right]
  my-aligned-box(left + bottom)[Bottom left]
  my-aligned-box(bottom)[Bottom]
  my-aligned-box(right + bottom)[Bottom right]
})
// :alignment}

=== Side note: lengths

Thanks to a #link("meta.html#_src/t2h/css.typ")[type-based translation from Typst to CSS],
you can actually use any of Typst's length types wherever the CSS expects a length.
In addition, you can also directly use whatever string is valid CSS for a length,
e.g. in pixels which is not a valid Typst unit of length.
#excerpt.incl(this, "lengths")

// {lengths:
#let as-len(l) = if type(l) == array { l.at(0) } else { l }
#let as-repr(l) = if type(l) == array { l.at(1) } else { raw(repr(l)) }
#let lengths = (
  100%, 25%, 100pt, (5cm, `5cm`), (50% + 1cm, `50% + 1cm`),
  50%, (50% - 1cm, `50% - 1cm`), 50% + 3em, (50% + 3em + 1cm, `50% + 3em + 1cm`),
  "300px", "calc(50% + 200px)",
)
#box(fill: "var(--dk-gray2)", width: 100%,
  align(left, {
    for l in lengths {
      orange-box(width: as-len(l))[#as-repr(l)]
      struct.box-linebreak // A regular linebreak wouldn't work here, unfortunately.
    }
  })
)
// :lengths}

=== Side note: colors

In the same way, there are multiple methods to define colors
- Typst colors, including `rgb`, `cmyk`, `luma`,
  and #link("https://typst.app/docs/reference/visualize/color")[more]
- whatever is supported natively by CSS, including standard named colors,
  hexadecimal, CSS global variables and
  #link("https://www.w3schools.com/cssref/css_colors.php")[more]

#excerpt.incl(this, "colors")

// {colors:
#let as-color(c) = if type(c) == array { c.at(0) } else { c }
#let as-repr(c) = if type(c) == array { c.at(1) } else { raw(repr(c)) }
#let colors = (
  (aqua, `aqua`), (rgb(10, 50, 200), `rgb(10, 50, 200)`),
  (rgb(80%, 50%, 5%), `rgb(80%, 50%, 5%)`), rgb("#aaaaff"),
  (luma(50%), `luma(50%)`), (color.hsv(60deg, 50%, 30%), `color.hsv(60deg, 50%, 30%)`),
  (red.negate(), `red.negate()`),
  (red.darken(50%), `red.darken(50%)`), (blue.transparentize(80%), `blue.transparentize(80%)`),
  "#fa1419", "blue", "YellowGreen", "var(--dk-orange)", "rgb(200, 30, 10)",
  "rgba(200, 30, 10, 0.5)", "hsl(110, 80%, 30%)",
)
#let cbox = box(inline: false, class: "cbox", inset: 1mm, outset: 1mm, radius: 1mm)
#box(fill: "var(--black)", width: 100%,
  for c in colors {
    cbox(fill: as-color(c))[#as-repr(c)]
  }
)
// :colors}

=== Side note: hover

When you bind a style to a class, you can manually insert `:hover` properties:
#excerpt.incl(this, "hover-demo")

// TODO: let box dictate the style of the inner text
// TODO: allow the :hover to be included in the box style
// {hover-demo:
#let gray-box = box(inline: false, class: "highlightable",
  fill: "var(--dk-gray2)", inset: 5mm,
)
#css.elems((
  ".highlightable": (
    transition: "0.3s",
  ),
  ".highlightable:hover": struct.box-style((:),
    fill: "var(--dk-red)",
    radius: (top-left: 5mm),
  )
))

#gray-box[Hover over me]
// :hover-demo}

== Spacing

#box(width: 100%)[#align(left)[
This is #html.div(style: css.raw-style((min-width: 5cm, background: red)))[] a test
]]

#box(width: 100%)[#align(left)[
This is #html.div(style: css.raw-style((width: 100%, min-height: 1cm, background: red)))[] another test.
]]

== More coming soon...

I will keep updating this page occasionally if I find an interesting trick,
or simply to implement more features. I have plans for:
- more options on already implemented elements
- justified text
- horizontal and vertical spaces
- images
- hrule

Don't hesitate to browse the source code in more detail, either #link("meta.html")[here]
or #link("https://github.com/vanille-n/website/tree/master/data/typ2html")[on the repo].

If you have suggestions you can open an
#link("https://github.com/login?return_to=https://github.com/Vanille-N/website/issues")[issue]
or #link("https://github.com/Vanille-N/website/compare")[pull request].

// TODO: there's something to do with links in general.

#include "/_src/footer.typ"

/* _src/pages/meta.typ */
#import "/_src/t2h/mod.typ": html, css, excerpt

#include "/_src/common.typ"

= Source code of #link("index.html")[Typ2HTML]

Also available directly
#link("https://github.com/vanille-n/website/tree/master/data/typ2html")[on the repo].

#let print(path, lang: "typst") = {
  html.h2(class: "header-link", id: path, raw(path))
  excerpt.full(path, lang: lang)
}

#print("justfile", lang: "make")
#print("_assets/global.css", lang: "css")

#{
  for t2hfile in ("mod", "html", "css", "struct", "excerpt") {
    print("_src/t2h/" + t2hfile + ".typ")
  }

  for auxfile in ("common", "footer") {
    print("_src/" + auxfile + ".typ")
  }

  for pagefile in ("index", "meta") {
    print("_src/pages/" + pagefile + ".typ")
  }
}

#include "/_src/footer.typ"