diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Kodama.toml | 32 | ||||
| -rw-r--r-- | publish/index.html | 141 | ||||
| -rw-r--r-- | publish/kodama.graph.json | 1 | ||||
| -rw-r--r-- | publish/kodama.json | 1 | ||||
| -rw-r--r-- | publish/main.css | 697 | ||||
| -rw-r--r-- | publish/main.js | 362 | ||||
| -rw-r--r-- | themes/moon_gbrls.html | 103 | ||||
| -rw-r--r-- | trees/_lib/kodama.typ | 415 | ||||
| -rw-r--r-- | trees/index.typst | 9 |
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! + + |
