summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-03-25 13:48:52 -0300
committerGabriel Schneider <[email protected]>2026-03-25 13:49:41 -0300
commitffb30c53b76adbee64fc6b0df59ab7599dec3d9e (patch)
tree344b5c1424e3099ac0110fafcabd2c67d51f864f
download0x4200.cafe-ffb30c53b76adbee64fc6b0df59ab7599dec3d9e.tar.gz
0x4200.cafe-ffb30c53b76adbee64fc6b0df59ab7599dec3d9e.zip
initial kodama site
-rw-r--r--.gitignore3
-rw-r--r--Kodama.toml32
-rw-r--r--publish/index.html141
-rw-r--r--publish/kodama.graph.json1
-rw-r--r--publish/kodama.json1
-rw-r--r--publish/main.css697
-rw-r--r--publish/main.js362
-rw-r--r--themes/moon_gbrls.html103
-rw-r--r--trees/_lib/kodama.typ415
-rw-r--r--trees/index.typst9
10 files changed, 1764 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bf94299
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Generated by Kodama
+/.cache
+.DS_Store
diff --git a/Kodama.toml b/Kodama.toml
new file mode 100644
index 0000000..60846fa
--- /dev/null
+++ b/Kodama.toml
@@ -0,0 +1,32 @@
+[kodama]
+trees = "trees"
+assets = "assets"
+base-url = "/"
+themes = ["themes/moon_gbrls.html"]
+
+[toc]
+placement = "right"
+sticky = true
+mobile-sticky = true
+max-width = "45ex"
+
+[text]
+edit = "[edit]"
+toc = "Table of Contents"
+references = "References"
+backlinks = "Backlinks"
+
+[build]
+typst-root = "trees"
+short-slug = false
+pretty-urls = false
+footer-mode = "link"
+inline-css = false
+inline-script = false
+asref = false
+output = "./publish"
+
+[serve]
+edit = "vscode://file/"
+output = "./.cache/publish"
+command = ["miniserve", "<output>", "--index", "index.html", "--pretty-urls"]
diff --git a/publish/index.html b/publish/index.html
new file mode 100644
index 0000000..1ef99a4
--- /dev/null
+++ b/publish/index.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<html lang="en-US"><head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta name="viewport" content="width=device-width"><title>index</title><link rel="icon" href="/assets/favicon.ico" /><template id="theme-option-template"><input type="radio" name="theme" /><label></label></template><script src="/main.js"></script><!-- Also see: https://katex.org/docs/autorender -->
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" integrity="sha384-WcoG4HRXMzYzfCgiyfrySxx90XSl2rxY5mnVY5TwtWE6KLrArNKn0T/mOgNL0Mmi" crossorigin="anonymous">
+<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js" integrity="sha384-J+9dG2KMoiR9hqcFao0IBLwxt6zpcyN68IgwzsCSkbreXUjmNVRhPFTssqdSGjwQ" crossorigin="anonymous"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js" integrity="sha384-hCXGrW6PitJEwbkoStFjeJxv+fSOOQKOPbJxSfM6G5sWZjAyWhXiTIIAmQqnlLlh" crossorigin="anonymous"></script>
+<!-- Also see: https://github.com/KaTeX/KaTeX/tree/main/contrib/copy-tex -->
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/copy-tex.min.js" integrity="sha384-HORx6nWi8j5/mYA+y57/9/CZc5z8HnEw4WUZWy5yOn9ToKBv1l58vJaufFAn9Zzi" crossorigin="anonymous"></script>
+
+<script>
+ document.addEventListener("DOMContentLoaded", function () {
+ renderMathInElement(document.body, {
+ delimiters: [
+ { left: "$$", right: "$$", display: true },
+ { left: "$", right: "$", display: false },
+ { left: "\\(", right: "\\)", display: false },
+ { left: "\\[", right: "\\]", display: true },
+ ],
+ trust: true,
+ strict: false,
+ throwOnError: false,
+ minRuleThickness: 0.05,
+ });
+ });
+</script>
+<link rel="stylesheet" href="/main.css"><style>
+@media only screen and (min-width: 1000px) {
+ #grid-wrapper { grid-template-columns: var(--article-max-width) var(--toc-max-width); }
+ nav#toc { max-width: 45ex; }
+}
+</style><link rel="preconnect" href="https://fonts.googleapis.com" />
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+<link href="https://fonts.googleapis.com/css2?family=Inria+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet" />
+<link href="https://fonts.googleapis.com/css2?family=LXGW+WenKai+TC:wght@400;700&display=swap" rel="stylesheet">
+</head><body><div id="grid-wrapper" style="grid-template-areas: 'article toc';" data-base-url="/"><nav id="toc" class="sticky-nav mobile-sticky-nav"><div id="theme-options"><!-- Copyright (c) 2025 Kodama Project. All rights reserved. -->
+<!-- Released under the Apache-2.0 license as described in the file LICENSE. -->
+<!-- Authors: Kokic (@kokic) -->
+<!-- Inspired by Pink Floyd - The Dark Side of the Moon -->
+
+<style>
+ body:has(input[value="moon"]:checked) {
+ --hover-primary-color: 181, 140, 114;
+
+ --text-color: rgb(32, 28, 48);
+ --toc-link-color: #555;
+ --background-color: rgb(255, 238, 228);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.1);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+
+ --link-color: rgb(227, 66, 114);
+ --slug-color: rgb(184, 170, 161);
+ --logo-color: #666;
+ --logo-hover-color: #aaa;
+ --span-taxon-color: black;
+ --article-taxon-color: #888;
+ --mark-color: rgb(255, 255, 151);
+ --em-color: rgb(220, 121, 113);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body:has(input[value="moon"]:checked) {
+ --hover-primary-color: 196, 136, 193;
+
+ --text-color: rgb(187 176 169);
+ --toc-link-color: rgb(187 176 169);
+ --background-color: rgb(0, 0, 0);
+
+ --background-color-pre: rgba(100, 100, 100, 0.2);
+ --background-color-code: rgba(150, 150, 150, 0.2);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.06);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+ --target-color: rgb(255, 255, 151);
+
+ --link-color: rgb(255, 128, 165);
+ --slug-color: #aeaeae;
+ --logo-color: #999;
+ --logo-hover-color: #eee;
+ --span-taxon-color: rgb(236, 230, 226);
+ --em-color: rgb(255, 219, 230);
+
+ .block:has(.block:hover) {
+ background-color: var(--background-color);
+ }
+ }
+ }
+
+ body:has(input[value="moon-light"]:checked) {
+ --hover-primary-color: 181, 140, 114;
+
+ --text-color: rgb(32, 28, 48);
+ --toc-link-color: #555;
+ --background-color: rgb(255, 238, 228);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.1);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+
+ --link-color: rgb(227, 66, 114);
+ --slug-color: rgb(184, 170, 161);
+ --logo-color: #666;
+ --logo-hover-color: #aaa;
+ --span-taxon-color: black;
+ --article-taxon-color: #888;
+ --mark-color: rgb(255, 255, 151);
+ --em-color: rgb(220, 121, 113);
+ }
+
+ body:has(input[value="moon-dark"]:checked) {
+ --hover-primary-color: 196, 136, 193;
+
+ --text-color: rgb(187 176 169);
+ --toc-link-color: rgb(187 176 169);
+ --background-color: rgb(32, 28, 48);
+
+ --background-color-pre: rgba(100, 100, 100, 0.2);
+ --background-color-code: rgba(150, 150, 150, 0.2);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.06);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+ --target-color: rgb(255, 255, 151);
+
+ --link-color: rgb(255, 128, 165);
+ --slug-color: #aeaeae;
+ --logo-color: #999;
+ --logo-hover-color: #eee;
+ --span-taxon-color: rgb(236, 230, 226);
+ --em-color: rgb(255, 219, 230);
+
+ .block:has(.block:hover) {
+ background-color: var(--background-color);
+ }
+ }
+</style>
+
+<theme-option name="moon">dark</theme-option>
+<theme-option name="moon-light">light</theme-option>
+</div></nav>
+
+<article><section class="block" data-taxon=""><details open><summary id="index"><header><h1><span class="taxon"></span>index <a class="slug" href="/index.html">[index]</a></h1><div class="metadata"><ul></ul></div></header></summary></details></section><footer></footer></article></div></body></html> \ No newline at end of file
diff --git a/publish/kodama.graph.json b/publish/kodama.graph.json
new file mode 100644
index 0000000..0f6304d
--- /dev/null
+++ b/publish/kodama.graph.json
@@ -0,0 +1 @@
+{"sections":{"index":{"parent":"index","parent_specified":false,"references":[],"backlinks":[]}}} \ No newline at end of file
diff --git a/publish/kodama.json b/publish/kodama.json
new file mode 100644
index 0000000..5315150
--- /dev/null
+++ b/publish/kodama.json
@@ -0,0 +1 @@
+{"index":{"slug":{"Plain":"index"},"ext":{"Plain":"md"},"title":{"Plain":"index"},"page-title":{"Plain":"index"}}} \ No newline at end of file
diff --git a/publish/main.css b/publish/main.css
new file mode 100644
index 0000000..f3ca6cb
--- /dev/null
+++ b/publish/main.css
@@ -0,0 +1,697 @@
+html {
+ font-size: 18px;
+}
+
+:root body {
+ color-scheme: light dark;
+ --content-gap: 15px;
+ --radius: 5px;
+ --article-max-width: 90ex;
+ --toc-max-width: 90ex;
+ --text-font-family: "Inria Sans", sans-serif;
+ --kaiti-font-family: "LXGW WenKai TC", "FandolKai", "KaiTi", "SimKai", "STKaiti", "Kaiti SC", "Kaiti TC", "华文楷体", "楷体", serif;
+
+ /* root font size: 16px */
+ --pre-font-size: 1rem;
+ --code-font-size: 1rem;
+ --cjk-kaiti-font-size: 1rem;
+ /* If this value is not 1rem, the CJK text in Typst will not match the size of the CJK text in KaTeX. */
+ --katex-font-size: 1em;
+ --katex-frac-line-font-size: 1.25em;
+ --details-h1-font-size: 1.2rem;
+ /* 13pt */
+ --section-details-h1-font-size: 1.083rem;
+ --article-details-h1-font-size: 1.5rem;
+ /* 0.9em */
+ --article-details-h1-taxon-font-size: 1.35rem;
+ /* 24px; */
+ --logo-font-size: 1.5rem;
+ /* 14pt; */
+ --footer-h2-font-size: 1.167rem;
+ --p-line-height: 1.55rem;
+
+ --text-color: black;
+ --toc-link-color: #555;
+ --background-color: white;
+
+ --background-color-pre: rgba(0, 100, 100, 0.04);
+ --background-color-code: rgba(0, 100, 100, 0.04);
+
+ --hover-color-block: rgba(0, 100, 255, 0.04);
+ --hover-color-link: rgba(0, 100, 255, 0.1);
+ --target-color: rgb(67, 92, 255);
+
+ --link-color: black;
+ --slug-color: gray;
+ --logo-color: #666;
+ --logo-hover-color: #aaa;
+ --span-taxon-color: #444;
+ --article-taxon-color: #888;
+ --mark-color: rgb(255, 255, 151);
+ --em-color: var(--text-color);
+
+ --alert-border-color: gray;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root body {
+ --text-color: white;
+ --toc-link-color: white;
+ --background-color: #2f2f2f;
+
+ --background-color-pre: rgba(100, 100, 100, 0.2);
+ --background-color-code: rgba(150, 150, 150, 0.2);
+
+ --hover-color-block: rgba(100, 162, 255, 0.06);
+ --hover-color-link: rgba(100, 162, 255, 0.2);
+ --target-color: rgb(255, 255, 151);
+
+ --link-color: white;
+ --slug-color: #aeaeae;
+ --logo-color: #999;
+ --logo-hover-color: #eee;
+ --span-taxon-color: #bbb;
+ }
+}
+
+@media print {
+ html {
+ font-size: 16px;
+ }
+
+ .edit,
+ #theme-options {
+ display: none;
+ }
+}
+
+.cursor-pointer {
+ cursor: pointer;
+}
+
+theme-option {
+ display: inline-block;
+}
+
+theme-option>input {
+ display: none;
+}
+
+theme-option>label {
+ color: var(--slug-color);
+}
+
+theme-option>label:hover {
+ background-color: var(--hover-color-link);
+}
+
+theme-option:has(input:checked)>label {
+ color: var(--link-color);
+}
+
+theme-option>label::before {
+ content: "[";
+ color: rgba(0, 0, 0, 0);
+}
+
+theme-option>label::after {
+ content: "]";
+ color: rgba(0, 0, 0, 0);
+}
+
+theme-option:has(input:checked)>label::before {
+ color: var(--link-color);
+}
+
+theme-option:has(input:checked)>label::after {
+ color: var(--link-color);
+}
+
+#theme-options::before {
+ content: "(";
+}
+
+#theme-options::after {
+ content: ")";
+}
+
+body {
+ color: var(--text-color);
+ font-optical-sizing: auto;
+ font-size: 1rem;
+ font-family: var(--text-font-family);
+ hyphens: auto;
+ background-color: var(--background-color);
+}
+
+pre,
+code {
+ font-optical-sizing: auto;
+}
+
+p,
+pre {
+ line-height: var(--p-line-height);
+}
+
+pre {
+ border-radius: var(--radius);
+ background-color: var(--background-color-pre);
+ padding: 0.5rem;
+ font-size: var(--pre-font-size);
+ margin-top: 0rem;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+}
+
+code {
+ border-radius: var(--radius);
+ background-color: var(--background-color-code);
+ padding: 0.1rem;
+ font-size: var(--code-font-size);
+}
+
+pre,
+code {
+ font-family: monospace;
+}
+
+table {
+ border-collapse: collapse;
+ margin-bottom: 1rem;
+}
+
+/* !forest */
+th {
+ font-weight: normal;
+ text-align: left;
+}
+
+th,
+td {
+ padding: 0 15px;
+ vertical-align: top;
+}
+
+/* !forest */
+pre>code {
+ border-radius: 0;
+ background-color: transparent;
+ padding: 0;
+}
+
+/* !forest */
+.footnote-definition:last-child {
+ margin-bottom: 1rem;
+}
+
+/* !forest */
+.footnote-definition>p {
+ display: inline;
+}
+
+/* !forest */
+.footnote-definition-label {
+ margin-right: 4px;
+}
+
+/* !forest */
+.footnote-reference:target {
+ border: 1px solid var(--target-color);
+}
+
+.footnote-definition:target>.footnote-definition-label {
+ border: 1px solid var(--target-color);
+}
+
+.footnote-reference::before,
+.footnote-definition-label::before {
+ content: "[";
+}
+
+.footnote-reference::after,
+.footnote-definition-label::after {
+ content: "]";
+}
+
+.katex {
+ font: normal var(--katex-font-size) KaTeX_Main, Times New Roman, serif;
+ line-height: 1rem;
+ text-indent: 0;
+ text-rendering: auto;
+}
+
+.katex .mfrac .frac-line {
+ font-size: var(--katex-frac-line-font-size);
+}
+
+/* Generated by KaTeX */
+.cjk_fallback {
+ font-size: 1rem;
+ font-family: var(--text-font-family);
+}
+
+p:has(span.katex-html:empty) {
+ display: none;
+}
+
+section+p:empty {
+ display: none;
+}
+
+.katex-display {
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.katex-display .katex {
+ /* Inconsistent bounding box at different font sizes. */
+ /* Also see: https://github.com/KaTeX/KaTeX/issues/3965 */
+ padding-top: 1px;
+}
+
+.display-none {
+ display: none;
+}
+
+em {
+ color: var(--em-color);
+ font-style: italic;
+}
+
+em span:is([lang=zh], [lang=ja], [lang=ko]) {
+ font-style: normal;
+ font-family: var(--kaiti-font-family);
+ font-size: var(--cjk-kaiti-font-size);
+}
+
+blockquote {
+ font-style: italic;
+ --blockquote-cjk-font-family: var(--kaiti-font-family);
+}
+
+blockquote span:is([lang=zh], [lang=ja], [lang=ko]) {
+ font-style: normal;
+ font-family: var(--blockquote-cjk-font-family);
+ font-size: var(--cjk-kaiti-font-size);
+}
+
+/* GFM Blockquote tags: https://github.com/orgs/community/discussions/16925 */
+
+blockquote:is(.markdown-alert-note, .markdown-alert-tip, .markdown-alert-important, .markdown-alert-warning, .markdown-alert-caution) {
+ --blockquote-cjk-font-family: var(--text-font-family);
+ margin: 0.5rem 0.5rem;
+ padding: 0rem 0.5rem;
+ font-style: normal;
+ border-left: .25rem double var(--alert-border-color);
+}
+
+blockquote:is(.markdown-alert-note, .markdown-alert-tip, .markdown-alert-important, .markdown-alert-warning, .markdown-alert-caution)>p:first-child::before {
+ font-size: var(--section-details-h1-font-size);
+ font-family: monospace;
+ font-style: normal;
+ color: var(--span-taxon-color);
+ margin-right: 0.5rem;
+ display: block;
+ content: var(--alert-label);
+}
+
+blockquote.markdown-alert-note {
+ --alert-label: "[# NOTE]";
+}
+
+blockquote.markdown-alert-tip {
+ --alert-label: "[# TIP]";
+}
+
+blockquote.markdown-alert-important {
+ --alert-label: "[# IMPORTANT]";
+}
+
+blockquote.markdown-alert-warning {
+ --alert-label: "[# WARNING]";
+}
+
+blockquote.markdown-alert-caution {
+ --alert-label: "[# CAUTION]";
+}
+
+h1,
+h2,
+h3,
+h4 {
+ margin-top: 0.5em;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-weight: 500;
+ margin-bottom: 0;
+}
+
+h5,
+h6,
+p {
+ margin-top: 0;
+}
+
+details>summary {
+ list-style-type: none;
+ outline: none;
+}
+
+details>summary>header {
+ display: inline;
+}
+
+details>summary::marker,
+details>summary::-webkit-details-marker {
+ display: none;
+}
+
+details h1 {
+ font-size: var(--details-h1-font-size);
+ display: inline;
+}
+
+span.taxon {
+ color: var(--span-taxon-color);
+ font-weight: 500;
+}
+
+article>section>details>summary>header>h1 {
+ font-size: var(--article-details-h1-font-size);
+}
+
+article>section>details>h2 {
+ font-size: 1.25rem;
+}
+
+article>section>details>h3 {
+ font-size: 1.125rem;
+}
+
+article>section>details>h4 {
+ font-size: 1rem;
+}
+
+article>section>details>h5,
+article>section>details>h6 {
+ font-size: 0.875rem;
+}
+
+article>section>details>summary>header {
+ display: block;
+ margin-bottom: 0.5em;
+}
+
+article>section>details>summary>header>h1>.taxon {
+ display: block;
+ font-size: var(--article-details-h1-taxon-font-size);
+ color: var(--article-taxon-color);
+ padding-bottom: 5pt;
+}
+
+article>section>details>summary>header>h1>.taxon:empty {
+ display: none;
+}
+
+section.block>details {
+ margin-bottom: 0.4em;
+}
+
+section.block>details[open] {
+ margin-bottom: 1em;
+}
+
+section.block.link-list>details {
+ margin-bottom: 0em;
+}
+
+section.block.link-list>details>section.block>details {
+ margin-bottom: 0.25em;
+}
+
+/* class */
+.inline-typst {
+ display: inline;
+ margin: 0 0;
+ font-size: var(--katex-font-size);
+}
+
+.block {
+ padding-left: 5px;
+ padding-right: 10px;
+ padding-bottom: 2px;
+ border-radius: var(--radius);
+}
+
+.block:hover {
+ background-color: var(--hover-color-block);
+}
+
+.block.hide-metadata>details>summary>header>.metadata {
+ display: none;
+}
+
+img {
+ object-fit: cover;
+ max-width: 100%;
+}
+
+figure {
+ text-align: center;
+}
+
+figcaption {
+ font-style: italic;
+ padding: 3px;
+}
+
+mark {
+ background-color: var(--mark-color);
+}
+
+hr {
+ margin-top: 10px;
+ margin-bottom: 20px;
+ background-color: gray;
+ border: 0 none;
+ width: 100%;
+ height: 1pt;
+}
+
+ul,
+ol {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+.logo>span {
+ color: var(--logo-color);
+ text-decoration: none;
+}
+
+.logo>span:hover {
+ color: var(--logo-hover-color);
+}
+
+.logo {
+ font-weight: 600;
+ font-size: var(--logo-font-size);
+}
+
+section section[data-taxon="Reference"]>details>summary>header>h1>.taxon,
+section section[data-taxon="Person"]>details>summary>header>h1>.taxon {
+ display: none;
+}
+
+section .block[data-taxon]>header>h1,
+section .block[data-taxon] details>summary>header>h1 {
+ font-size: var(--section-details-h1-font-size);
+}
+
+footer>section {
+ margin-bottom: 1em;
+}
+
+footer h2 {
+ font-size: var(--footer-h2-font-size);
+}
+
+.metadata ul {
+ padding-left: 0;
+ display: inline;
+}
+
+.metadata li::after {
+ content: " · ";
+}
+
+.metadata li:last-child::after {
+ content: "";
+}
+
+.metadata * {
+ display: inline;
+}
+
+.link {
+ cursor: pointer;
+}
+
+a {
+ color: var(--link-color);
+ text-decoration: inherit;
+}
+
+a.slug:hover,
+a.edit:hover,
+a.hash:hover,
+a.bullet:hover,
+.link:hover {
+ background-color: var(--hover-color-link);
+}
+
+.link.external,
+.link.asset {
+ text-decoration: underline;
+}
+
+.link.asset::before {
+ content: "[F] ";
+ /* file */
+}
+
+a.link.local,
+.link.local a,
+a.slug,
+a.edit,
+a.hash {
+ box-shadow: none;
+ text-decoration-line: underline;
+ text-decoration-style: dotted;
+ overflow-wrap: break-word;
+}
+
+.slug,
+.edit,
+.doi,
+.orcid {
+ color: var(--slug-color);
+ font-weight: 200;
+}
+
+body>header {
+ margin-bottom: 0.5em;
+}
+
+#grid-wrapper>article {
+ max-width: var(--article-max-width);
+ margin-right: auto;
+ grid-area: article;
+}
+
+@media only screen and (max-width: 1000px) {
+ :root body {
+ --code-font-size: 0.9rem;
+ }
+
+ figure {
+ margin: 0rem;
+ }
+
+ .block {
+ padding-left: 2px;
+ padding-right: 2px;
+ padding-bottom: 2px;
+ border-radius: 5px;
+ }
+
+ #grid-wrapper>nav {
+ transition: ease all 0.2s;
+ }
+
+ .mobile-sticky-nav {
+ position: sticky;
+ top: 0px;
+ max-height: calc(100vh - 0px);
+ overflow-y: auto;
+ scrollbar-width: thin;
+
+ background-color: var(--background-color);
+ z-index: 7;
+ border-bottom: solid var(--text-color);
+ }
+}
+
+@media only screen and (min-width: 1000px) {
+ body {
+ margin-top: 2rem;
+ margin-left: 2rem;
+ transition: ease all 0.2s;
+ }
+
+ #grid-wrapper {
+ display: grid;
+ grid-auto-flow: column;
+ }
+
+ .sticky-nav {
+ position: sticky;
+ top: 0px;
+ max-height: calc(100vh - 0px);
+ overflow-y: auto;
+ scrollbar-width: thin;
+ }
+}
+
+nav#toc li.item-summary li {
+ display: none;
+}
+
+nav#toc ul {
+ list-style-type: none;
+}
+
+nav#toc li>ul {
+ margin: 0;
+ padding-left: 1rem;
+}
+
+nav#toc,
+nav#toc a {
+ color: var(--toc-link-color);
+}
+
+nav#toc {
+ grid-area: toc;
+}
+
+nav {
+ font-optical-sizing: auto;
+}
+
+nav#toc a.bullet {
+ opacity: 0.7;
+ margin-left: 0.4rem;
+ margin-right: 0.3rem;
+ padding-left: 0.2rem;
+ padding-right: 0.2rem;
+ text-decoration: none;
+}
+
+.typst-text use[fill="#000000"],
+.typst-group use[fill="#000000"],
+path.typst-shape[fill="#000000"] {
+ fill: var(--text-color);
+}
+
+path.typst-shape[stroke="#000000"] {
+ stroke: var(--text-color);
+} \ No newline at end of file
diff --git a/publish/main.js b/publish/main.js
new file mode 100644
index 0000000..5caefb6
--- /dev/null
+++ b/publish/main.js
@@ -0,0 +1,362 @@
+// Mobile TOC: responsive open / close behavior for the table of contents.
+function toggleDetailsOpen() {
+ const details = document.querySelector("#toc>div>details");
+ if (!details) return;
+
+ if (window.matchMedia("(max-width: 1000px)").matches) {
+ details.removeAttribute("open");
+ } else {
+ details.setAttribute("open", "");
+ }
+}
+
+document.addEventListener("DOMContentLoaded", toggleDetailsOpen);
+window.addEventListener("resize", toggleDetailsOpen);
+
+// Theme: option selection, persistence, and dynamic color invert logic.
+const KODAMA_THEME_KEY = `kodama-theme`;
+
+function storeSelectedTheme(name) {
+ localStorage.setItem(KODAMA_THEME_KEY, name);
+}
+
+function getCurrentTheme() {
+ return localStorage.getItem(KODAMA_THEME_KEY) || window.primaryTheme;
+}
+
+function selectTheme(themeName) {
+ storeSelectedTheme(themeName);
+ requestAnimationFrame(applyDynamicColorInvert);
+}
+
+function applyFavorTheme() {
+ const favoredTheme = getCurrentTheme();
+ if (favoredTheme) {
+ const selector = `input[type='radio'][id='${favoredTheme}']`;
+ const favoredOption = window.themeOptions.querySelector(selector);
+
+ if (favoredOption) {
+ favoredOption.checked = true;
+ } else if (window.primaryOption) {
+ // Fallback to primary theme, if the favored theme is not found
+ // e.g., when the theme options have changed, and the stored theme is no longer available.
+ window.primaryOption.checked = true;
+ }
+ }
+ applyDynamicColorInvert();
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+ window.themeOptions = document.getElementById("theme-options");
+ const templateContent = document.getElementById("theme-option-template").content;
+
+ customElements.define(
+ "theme-option",
+ class extends HTMLElement {
+ constructor() {
+ super();
+
+ const node = templateContent.cloneNode(true);
+ const themeName = this.getAttribute("name");
+
+ const input = node.querySelector("input");
+ input.setAttribute("id", themeName);
+ input.setAttribute("value", themeName);
+
+ const label = node.querySelector("label");
+ label.setAttribute("for", themeName);
+ label.addEventListener("click", () => selectTheme(themeName));
+
+ while (this.firstChild) {
+ label.appendChild(this.firstChild);
+ }
+ this.appendChild(node);
+ }
+ },
+ );
+
+ window.primaryOption = window.themeOptions.querySelector("input[type='radio']");
+ window.primaryTheme = primaryOption?.value;
+
+ applyFavorTheme();
+});
+
+/// SPSA filter solver
+///
+/// Also see:
+/// - https://en.wikipedia.org/wiki/Simultaneous_perturbation_stochastic_approximation
+/// - https://github.com/pranjalworm/css-filter-generator
+///
+/// Principle
+///
+/// invert → sepia → saturate → hue-rotate → brightness → contrast
+/// loss = |ΔR| + |ΔG| + |ΔB| + |ΔH| + |ΔS| + |ΔL|
+///
+
+const clamp = (value) => Math.max(0, Math.min(255, value));
+
+const multiply = ([r, g, b], matrix) => {
+ const nr = clamp(r * matrix[0] + g * matrix[1] + b * matrix[2]);
+ const ng = clamp(r * matrix[3] + g * matrix[4] + b * matrix[5]);
+ const nb = clamp(r * matrix[6] + g * matrix[7] + b * matrix[8]);
+ return [nr, ng, nb];
+};
+
+const hueRotate = ([r, g, b], angle = 0) => {
+ const rad = (angle / 180) * Math.PI;
+ const sin = Math.sin(rad);
+ const cos = Math.cos(rad);
+ const matrix = [
+ 0.213 + cos * 0.787 - sin * 0.213,
+ 0.715 - cos * 0.715 - sin * 0.715,
+ 0.072 - cos * 0.072 + sin * 0.928,
+ 0.213 - cos * 0.213 + sin * 0.143,
+ 0.715 + cos * 0.285 + sin * 0.140,
+ 0.072 - cos * 0.072 - sin * 0.283,
+ 0.213 - cos * 0.213 - sin * 0.787,
+ 0.715 - cos * 0.715 + sin * 0.715,
+ 0.072 + cos * 0.928 + sin * 0.072,
+ ];
+ return multiply([r, g, b], matrix);
+};
+
+const grayscale = ([r, g, b], value = 1) => {
+ const matrix = [
+ 0.2126 + 0.7874 * (1 - value),
+ 0.7152 - 0.7152 * (1 - value),
+ 0.0722 - 0.0722 * (1 - value),
+ 0.2126 - 0.2126 * (1 - value),
+ 0.7152 + 0.2848 * (1 - value),
+ 0.0722 - 0.0722 * (1 - value),
+ 0.2126 - 0.2126 * (1 - value),
+ 0.7152 - 0.7152 * (1 - value),
+ 0.0722 + 0.9278 * (1 - value),
+ ];
+ return multiply([r, g, b], matrix);
+};
+
+const sepia = ([r, g, b], value = 1) => {
+ const matrix = [
+ 0.393 + 0.607 * (1 - value),
+ 0.769 - 0.769 * (1 - value),
+ 0.189 - 0.189 * (1 - value),
+ 0.349 - 0.349 * (1 - value),
+ 0.686 + 0.314 * (1 - value),
+ 0.168 - 0.168 * (1 - value),
+ 0.272 - 0.272 * (1 - value),
+ 0.534 - 0.534 * (1 - value),
+ 0.131 + 0.869 * (1 - value),
+ ];
+ return multiply([r, g, b], matrix);
+};
+
+const saturate = ([r, g, b], value = 1) => {
+ const matrix = [
+ 0.213 + 0.787 * value,
+ 0.715 - 0.715 * value,
+ 0.072 - 0.072 * value,
+ 0.213 - 0.213 * value,
+ 0.715 + 0.285 * value,
+ 0.072 - 0.072 * value,
+ 0.213 - 0.213 * value,
+ 0.715 - 0.715 * value,
+ 0.072 + 0.928 * value,
+ ];
+ return multiply([r, g, b], matrix);
+};
+
+const linear = ([r, g, b], slope = 1, intercept = 0) => {
+ const scale = 255;
+ return [
+ clamp(r * slope + intercept * scale),
+ clamp(g * slope + intercept * scale),
+ clamp(b * slope + intercept * scale),
+ ];
+};
+
+const brightness = (rgb, value = 1) => linear(rgb, value);
+const contrast = (rgb, value = 1) => linear(rgb, value, -0.5 * value + 0.5);
+
+const invert = ([r, g, b], value = 1) => {
+ const nr = (value + r / 255 * (1 - 2 * value)) * 255;
+ const ng = (value + g / 255 * (1 - 2 * value)) * 255;
+ const nb = (value + b / 255 * (1 - 2 * value)) * 255;
+ return [clamp(nr), clamp(ng), clamp(nb)];
+};
+
+// Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
+const rgbToHsl = (r, g, b) => {
+ r /= 255; g /= 255; b /= 255;
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ let h, s, l = (max + min) / 2;
+ if (max === min) {
+ h = s = 0;
+ } else {
+ const d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return { h: h * 100, s: s * 100, l: l * 100 };
+};
+
+const applyFilters = (rgb, [inv, sep, sat, hue, bri, con]) => {
+ let color = [...rgb];
+ color = invert(color, inv / 100);
+ color = sepia(color, sep / 100);
+ color = saturate(color, sat / 100);
+ color = hueRotate(color, hue * 3.6);
+ color = brightness(color, bri / 100);
+ color = contrast(color, con / 100);
+ return color;
+};
+
+const lossFn = (targetRgb, filters) => {
+ const resultRgb = applyFilters([0, 0, 0], filters);
+ const targetHsl = rgbToHsl(...targetRgb);
+ const resultHsl = rgbToHsl(...resultRgb);
+ return (
+ Math.abs(resultRgb[0] - targetRgb[0]) +
+ Math.abs(resultRgb[1] - targetRgb[1]) +
+ Math.abs(resultRgb[2] - targetRgb[2]) +
+ Math.abs(resultHsl.h - targetHsl.h) +
+ Math.abs(resultHsl.s - targetHsl.s) +
+ Math.abs(resultHsl.l - targetHsl.l)
+ );
+};
+
+const fix = (value, idx) => {
+ let max = 100;
+ if (idx === 2) max = 7500; // saturate
+ else if (idx === 4 || idx === 5) max = 200; // brightness, contrast
+ if (idx === 3) { // hue-rotate
+ if (value > max) return value % max;
+ if (value < 0) return max + (value % max);
+ return value;
+ }
+ return Math.max(0, Math.min(max, value));
+};
+
+const spsa = (targetRgb, A, a, c, initial, iters) => {
+ const alpha = 1;
+ const gamma = 1 / 6;
+ let values = [...initial];
+ let best = [...values];
+ let bestLoss = Infinity;
+
+ for (let k = 0; k < iters; k++) {
+ const ck = c / Math.pow(k + 1, gamma);
+ const deltas = Array(6).fill().map(() => (Math.random() > 0.5 ? 1 : -1));
+ const highArgs = values.map((v, i) => v + ck * deltas[i]);
+ const lowArgs = values.map((v, i) => v - ck * deltas[i]);
+ const lossDiff = lossFn(targetRgb, highArgs) - lossFn(targetRgb, lowArgs);
+
+ values = values.map((v, i) => {
+ const g = (lossDiff / (2 * ck)) * deltas[i];
+ const ak = a[i] / Math.pow(A + k + 1, alpha);
+ return fix(v - ak * g, i);
+ });
+
+ const loss = lossFn(targetRgb, values);
+ if (loss < bestLoss) {
+ best = [...values];
+ bestLoss = loss;
+ }
+ }
+ return { values: best, loss: bestLoss };
+};
+
+// Threshold for acceptable color loss. This value determines when the SPSA algorithm stops early.
+// A loss below this value is considered visually indistinguishable for most use cases.
+// The value 25 was determined empirically to balance performance and color accuracy.
+const ACCEPTABLE_COLOR_LOSS = 25;
+
+const solveWide = (targetRgb) => {
+ const A = 5;
+ const c = 15;
+ const a = [60, 180, 18000, 600, 1.2, 1.2];
+ let best = { loss: Infinity, values: null };
+ for (let i = 0; best.loss > ACCEPTABLE_COLOR_LOSS && i < 3; i++) {
+ const initial = [50, 20, 3750, 50, 100, 100];
+ const result = spsa(targetRgb, A, a, c, initial, 1000);
+ if (result.loss < best.loss) best = result;
+ }
+ return best;
+};
+
+const solveNarrow = (targetRgb, wideResult) => {
+ const A = wideResult.loss;
+ const c = 2;
+ const A1 = A + 1;
+ const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
+ return spsa(targetRgb, A, a, c, wideResult.values, 500);
+};
+
+const cssFilterString = (filters) => {
+ const [inv, sep, sat, hue, bri, con] = filters.map(Math.round);
+ const hueDeg = Math.round(hue * 3.6);
+ return `filter: invert(${inv}%) sepia(${sep}%) saturate(${sat}%) hue-rotate(${hueDeg}deg) brightness(${bri}%) contrast(${con}%);`;
+};
+
+const solveColor = (targetRgb) => {
+ const wide = solveWide(targetRgb);
+ const narrow = solveNarrow(targetRgb, wide);
+ return {
+ values: narrow.values,
+ loss: narrow.loss,
+ filter: cssFilterString(narrow.values),
+ };
+};
+
+// Reuse a single canvas / context to avoid memory leaks
+const colorParseCanvas = document.createElement('canvas');
+colorParseCanvas.width = 1;
+colorParseCanvas.height = 1;
+const colorParseContext = colorParseCanvas.getContext('2d');
+
+const toRGBArray = (cssColor) => {
+ colorParseContext.fillStyle = cssColor;
+ colorParseContext.fillRect(0, 0, 1, 1);
+
+ // The `getImageData().data` returns a Uint8ClampedArray with 4 values per pixel (RGBA).
+ // also see: https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData/data
+ const rgba = colorParseContext.getImageData(0, 0, 1, 1).data;
+ return Array.from(rgba.slice(0, 3));
+};
+
+const DYNAMIC_STYLE_ELEMENT_ID = 'dynamic-color-invert-style';
+
+function memorizedSolvedFilter(color) {
+ const colorKey = `solved-${color}`;
+ const storedFilterOption = localStorage.getItem(colorKey);
+ if (storedFilterOption) {
+ return storedFilterOption;
+ } else {
+ const filter = solveColor(color).filter;
+ localStorage.setItem(colorKey, filter);
+ return filter;
+ }
+}
+
+function applyDynamicColorInvert() {
+ const textColor = getComputedStyle(document.body).getPropertyValue('--text-color');
+ if (textColor && textColor.length > 0) {
+ const rgb = toRGBArray(textColor);
+ const filter = memorizedSolvedFilter(rgb);
+
+ const existingStyle = document.getElementById(DYNAMIC_STYLE_ELEMENT_ID);
+ if (existingStyle) {
+ existingStyle.textContent = `.color-invert {${filter}}`;
+ } else {
+ const style = document.createElement('style');
+ style.id = DYNAMIC_STYLE_ELEMENT_ID;
+ style.textContent = `.color-invert {${filter}}`;
+ document.head.appendChild(style);
+ }
+ }
+}
diff --git a/themes/moon_gbrls.html b/themes/moon_gbrls.html
new file mode 100644
index 0000000..ce5a275
--- /dev/null
+++ b/themes/moon_gbrls.html
@@ -0,0 +1,103 @@
+<!-- Copyright (c) 2025 Kodama Project. All rights reserved. -->
+<!-- Released under the Apache-2.0 license as described in the file LICENSE. -->
+<!-- Authors: Kokic (@kokic) -->
+<!-- Inspired by Pink Floyd - The Dark Side of the Moon -->
+
+<style>
+ body:has(input[value="moon"]:checked) {
+ --hover-primary-color: 181, 140, 114;
+
+ --text-color: rgb(32, 28, 48);
+ --toc-link-color: #555;
+ --background-color: rgb(255, 238, 228);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.1);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+
+ --link-color: rgb(227, 66, 114);
+ --slug-color: rgb(184, 170, 161);
+ --logo-color: #666;
+ --logo-hover-color: #aaa;
+ --span-taxon-color: black;
+ --article-taxon-color: #888;
+ --mark-color: rgb(255, 255, 151);
+ --em-color: rgb(220, 121, 113);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body:has(input[value="moon"]:checked) {
+ --hover-primary-color: 196, 136, 193;
+
+ --text-color: rgb(187 176 169);
+ --toc-link-color: rgb(187 176 169);
+ --background-color: rgb(0, 0, 0);
+
+ --background-color-pre: rgba(100, 100, 100, 0.2);
+ --background-color-code: rgba(150, 150, 150, 0.2);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.06);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+ --target-color: rgb(255, 255, 151);
+
+ --link-color: rgb(255, 128, 165);
+ --slug-color: #aeaeae;
+ --logo-color: #999;
+ --logo-hover-color: #eee;
+ --span-taxon-color: rgb(236, 230, 226);
+ --em-color: rgb(255, 219, 230);
+
+ .block:has(.block:hover) {
+ background-color: var(--background-color);
+ }
+ }
+ }
+
+ body:has(input[value="moon-light"]:checked) {
+ --hover-primary-color: 181, 140, 114;
+
+ --text-color: rgb(32, 28, 48);
+ --toc-link-color: #555;
+ --background-color: rgb(255, 238, 228);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.1);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+
+ --link-color: rgb(227, 66, 114);
+ --slug-color: rgb(184, 170, 161);
+ --logo-color: #666;
+ --logo-hover-color: #aaa;
+ --span-taxon-color: black;
+ --article-taxon-color: #888;
+ --mark-color: rgb(255, 255, 151);
+ --em-color: rgb(220, 121, 113);
+ }
+
+ body:has(input[value="moon-dark"]:checked) {
+ --hover-primary-color: 196, 136, 193;
+
+ --text-color: rgb(187 176 169);
+ --toc-link-color: rgb(187 176 169);
+ --background-color: rgb(32, 28, 48);
+
+ --background-color-pre: rgba(100, 100, 100, 0.2);
+ --background-color-code: rgba(150, 150, 150, 0.2);
+
+ --hover-color-block: rgba(var(--hover-primary-color), 0.06);
+ --hover-color-link: rgba(var(--hover-primary-color), 0.2);
+ --target-color: rgb(255, 255, 151);
+
+ --link-color: rgb(255, 128, 165);
+ --slug-color: #aeaeae;
+ --logo-color: #999;
+ --logo-hover-color: #eee;
+ --span-taxon-color: rgb(236, 230, 226);
+ --em-color: rgb(255, 219, 230);
+
+ .block:has(.block:hover) {
+ background-color: var(--background-color);
+ }
+ }
+</style>
+
+<theme-option name="moon">dark</theme-option>
+<theme-option name="moon-light">light</theme-option>
diff --git a/trees/_lib/kodama.typ b/trees/_lib/kodama.typ
new file mode 100644
index 0000000..495e85c
--- /dev/null
+++ b/trees/_lib/kodama.typ
@@ -0,0 +1,415 @@
+// Copyright (c) 2025 Kodama Project. All rights reserved.
+// Released under the GPL-3.0 license as described in the file LICENSE.
+// Authors: Alias Qli (@AliasQli), Kokic (@kokic)
+// Last modified time: 2026/03/06
+
+/**
+ * There are some external inputs:
+ * sys.inputs.path: relative path of the typst file
+ * sys.inputs.random: a random number in 0..INT64_MAX (note, it's a string)
+ */
+
+#let repri(r) = if type(r) == str { r } else {
+ repr(r)
+}
+
+#let with-target-check(callback) = context {
+ let target-value = if "target" in dictionary(std) { std.target() } else { "paged" }
+ callback(target-value)
+}
+
+#let compatibled-html(f, content-provider) = with-target-check((export-target) => {
+ let content = content-provider()
+ if export-target == "html" { f()(content) } else {
+ content
+ }
+})
+
+#let auto-frame(content) = compatibled-html(() => html.frame, () => content)
+#let auto-figure(content) = with-target-check((export-target) => {
+ if export-target == "html" {
+ html.figure(content) // main.css: `figure { text-align: center; }`
+ } else {
+ align(center, content)
+ }
+})
+
+#let html-font-size = 15.525pt
+
+// paged
+#let paged-metadata-text-color = gray
+#let small-block-below = 0.65em
+#let heading-font-weight = "black"
+#let slug-color = gray
+#let taxon-color = gray
+
+#let is_preset_key(key) = {
+ (
+ "title",
+ "taxon",
+ "parent",
+ "page-title",
+ "backlinks",
+ "transparent-backlinks",
+ "references",
+ "asref",
+ "asback",
+ "footer-mode",
+ ).contains(key)
+}
+
+#let dotted-stroke = (thickness: 0.1em, dash: ("dot", "dot")/* = thickness */)
+
+#let span-slug(slug) = underline(stroke: dotted-stroke, text(size: 1.083em, fill: slug-color, raw("[" + slug + "]")))
+
+#let taxon-upper(taxon) = upper(taxon.at(0)) + taxon.slice(1) + "."
+
+#let metadata(table) = {
+ let title = table.at("title", default: "")
+ let taxon = table.at("taxon", default: none)
+
+ let table-pairs = table.pairs()
+ let custom-pairs = table-pairs.filter(e => not is_preset_key(e.at(0)))
+
+ with-target-check(
+ (export-target) => {
+ if export-target == "html" {
+ table-pairs.map(e => {
+ let value = e.at(1)
+ let v = value
+ let attrs = (key: e.at(0))
+
+ if type(value) != content {
+ v = none
+ attrs.insert("value", repri(value))
+ }
+ html.elem("kodama-meta", v, attrs: attrs)
+ }).join()
+ } else {
+ if taxon != none {
+ text(weight: heading-font-weight, fill: taxon-color, size: 1.35em, taxon-upper(taxon))
+ }
+ block(above: small-block-below, below: small-block-below, text(size: 1.5em, weight: heading-font-weight, title))
+ block(custom-pairs.map(e =>
+ e.at(1)).join(text(" · ")))
+ }
+ },
+ )
+}
+
+#let external(dest, content) = link(dest, underline(content))
+
+///
+/// - raw-tex (string): raw TeX math source code without delimiters
+/// -> string
+#let tex(raw-tex) = "$" + raw-tex.text + "$"
+
+#let local(slug, text: none) = with-target-check((export-target) => {
+ if export-target == "html" {
+ html.elem(
+ "span", // Make it an inline element. This is automatically removed by kodama.
+ {
+ let v = if text == none { none } else { text }
+ let attrs = (slug: slug)
+
+ if text != none and type(text) != content {
+ v = none
+ attrs.insert("value", repri(text))
+ }
+
+ html.elem("kodama-local", v, attrs: attrs)
+ },
+ )
+ } else {
+ let label = if text == none { slug } else { text }
+ underline(stroke: dotted-stroke, label)
+ }
+})
+
+#let embed(url, title, numbering: false, open: true, catalog: true, display-options: false) = {
+ with-target-check((export-target) => {
+ if export-target == "html" {
+ let v = title
+ let attrs = (url: url, numbering: repri(numbering), open: repri(open), catalog: repri(catalog))
+
+ if type(title) != content {
+ v = none
+ attrs.insert("value", repri(title))
+ }
+
+ html.elem("kodama-embed", v, attrs: attrs)
+ } else {
+ block(below: small-block-below, text(size: 1.083em, weight: heading-font-weight, title))
+ if display-options {
+ block(text(fill: paged-metadata-text-color)[`numbering:` #numbering ~ `open:` #open ~ `toc:` #catalog])
+ }
+ }
+ })
+}
+
+#let subtree(
+ slug: none, // default: anonymous subtree
+ title: none,
+ taxon: none,
+ numbering: false,
+ open: true,
+ catalog: true,
+ content,
+) = with-target-check((export-target) => {
+ if export-target == "html" {
+ let attrs = (numbering: repri(numbering), open: repri(open), catalog: repri(catalog))
+ if slug != none { attrs.insert("slug", repri(slug)) }
+ if title != none { attrs.insert("title", repri(title)) }
+ if taxon != none { attrs.insert("taxon", repri(taxon)) }
+ html.elem("kodama-subtree", content, attrs: attrs)
+ } else {
+ block(below: small-block-below)[
+ #if taxon != none {
+ text(size: 1.083em, weight: heading-font-weight, fill: taxon-color, taxon-upper(taxon))
+ }
+ #text(size: 1.083em, weight: heading-font-weight, title)
+ #if slug != none { span-slug(slug) }
+ ]
+ content
+ }
+})
+
+// Semantic subtree sugar helpers aligned with markdown subtree tags.
+#let exegesis(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "exegesis" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let definition(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "definition" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let proposition(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "proposition" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let remark(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "remark" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let conjecture(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "conjecture" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let postulate(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "postulate" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let claim(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "claim" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let observation(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "observation" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let fact(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "fact" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let hypothesis(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "hypothesis" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let axiom(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "axiom" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let lemma(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "lemma" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let theorem(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "theorem" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let corollary(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "corollary" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let example(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "example" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+#let proof(slug: none, title: none, taxon: none, numbering: false, open: true, catalog: true, content) = subtree(
+ slug: slug,
+ title: title,
+ taxon: if taxon == none { "proof" } else { taxon },
+ numbering: numbering,
+ open: open,
+ catalog: catalog,
+ content,
+)
+
+
+/**
+ * HTML: SVG formula rendering vertical position adjustment
+ */
+
+#let bounded(eq) = text(top-edge: "bounds", bottom-edge: "bounds", eq)
+#let to-em(pt) = str(pt / text.size.pt()) + "em"
+
+// a dict that stores the height of equations
+#let equations-height-dict = state("eq_height_dict", (:))
+#let is-inside-pin = state("inside_pin", false)
+
+#let pin(label) = context {
+ let height = here().position().y
+ equations-height-dict.update(it => {
+ if label in it.keys() or height < 0.000001pt { it } else {
+ it.insert(label, height); it
+ }
+ })
+}
+
+#let add-pin(eq) = {
+ let label = repr(eq)
+ is-inside-pin.update(true)
+ $ inline(pin(label)#bounded(eq)) $
+ is-inside-pin.update(false)
+}
+
+#let kodama(doc) = {
+ with-target-check(
+ (export-target) => {
+ if export-target == "paged" {
+ set page(margin: 2em, paper: "iso-b6", height: auto)
+ set par(spacing: 1.5em)
+ doc
+ } else {
+ show math.equation.where(block: false): it => {
+ with-target-check(
+ (export-target) => {
+ if export-target == "html" {
+ let label = repr(it)
+ if label in equations-height-dict.final().keys() {
+ let height = equations-height-dict.final().at(label, default: none)
+ equations-height-dict.update(d => {
+ d.insert(label, height); d
+ })
+ let y-length = measure(bounded(it)).height
+ let shift = y-length - height
+ box(html.elem("span", attrs: (style: "vertical-align: -" + to-em(shift.pt()) + ";"), html.frame(bounded(it))))
+ } else {
+ box(html.frame(add-pin(it)))
+ }
+ } else {
+ it
+ }
+ },
+ )
+ }
+ show math.equation.where(block: true): it => {
+ with-target-check(
+ (export-target) => {
+ if export-target == "html" {
+ if is-inside-pin.get() {
+ html.frame(it)
+ } else {
+ html.elem("div", attrs: (style: "display: flex; justify-content: center; width: 100%; margin: 1em 0;"), html.frame(it))
+ }
+ } else {
+ it
+ }
+ },
+ )
+ }
+ doc
+ }
+ },
+ )
+}
diff --git a/trees/index.typst b/trees/index.typst
new file mode 100644
index 0000000..97aada1
--- /dev/null
+++ b/trees/index.typst
@@ -0,0 +1,9 @@
+#import "_lib/kodama.typ": *
+#show: kodama
+
+#metadata(("title": "0x4200.cafe"))
+
+
+hello!
+
+