Source code + assets for pluie.me

lots of stuff

pluie.me 49cdd265 a96984c6

verified
+742 -230
+8
.envrc
··· 1 + # only run some commands if nix is installed 2 + nix="$(command -v nix)" 3 + 4 + if ! has nix_direnv_version || ! nix_direnv_version 2.2.1 && [ -n "$nix" ] ; then 5 + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" 6 + fi 7 + 8 + [ -n "$nix" ] && use flake
+2 -1
.gitignore
··· 3 3 _site 4 4 .DS_Store 5 5 src/_cache 6 - deno.lock 6 + deno.lock 7 + .direnv
+9 -4
_config.ts
··· 5 5 import pug from "lume/plugins/pug.ts"; 6 6 import inline from "lume/plugins/inline.ts"; 7 7 import date from "lume/plugins/date.ts"; 8 + import vento from "./vento-improved.ts"; 8 9 9 10 // remark plugins 10 11 import remark from "lume/plugins/remark.ts"; ··· 16 17 import postcss from "lume/plugins/postcss.ts"; 17 18 import tailwindcss from "lume/plugins/tailwindcss.ts"; 18 19 import tailwindConfig from "./tailwind.config.ts"; 20 + import stripIndent from "npm:strip-indent"; 19 21 20 22 const site = lume({ src: "./src" }); 21 - 22 23 site 23 24 .use(remark({ remarkPlugins: [emoji, a11yEmoji, smartyPants] })) 24 25 .use(minify_html()) 25 26 .use(sass({ includes: "_styles" })) 26 - .use(tailwindcss({ 27 - options: tailwindConfig, 28 - })) 27 + .use( 28 + tailwindcss({ 29 + options: tailwindConfig, 30 + }) 31 + ) 29 32 .use(imagick()) 30 33 .use(pug()) 31 34 .use(inline()) 32 35 .use(date()) 33 36 .use(postcss()) 37 + .use(vento()) 38 + .filter("strip_indent", stripIndent) 34 39 .copy("assets", ".") 35 40 .copy("scripts", "scripts"); 36 41
+4 -10
flake.nix
··· 8 8 nixpkgs, 9 9 }: let 10 10 systems = ["x86_64-linux" "x86_64-darwin"]; 11 - forAllSystems = f: nixpkgs.lib.genAttrs systems f; 12 - nixpkgsFor = forAllSystems (system: 13 - import nixpkgs { 14 - inherit system; 15 - }); 11 + forAllSystems = f: nixpkgs.lib.genAttrs systems (s: f (import nixpkgs {system = s;})); 16 12 in { 17 - devShells = forAllSystems (system: { 18 - default = nixpkgsFor.${system}.mkShell { 19 - buildInputs = with nixpkgsFor.${system}; [ 20 - deno 21 - ]; 13 + devShells = forAllSystems (pkgs: { 14 + default = pkgs.mkShell { 15 + buildInputs = [pkgs.deno]; 22 16 }; 23 17 }); 24 18 };
+1 -1
import_map.json
··· 1 1 { 2 2 "imports": { 3 - "lume/": "https://deno.land/x/lume@v1.18.0/" 3 + "lume/": "https://deno.land/x/lume@v1.18.4/" 4 4 } 5 5 }
-16
src/404.pug
··· 1 - --- 2 - layout: layouts/default.pug 3 - title: "404" 4 - description: Oooops... page not found! 5 - 6 - url: /404.html 7 - centered: true 8 - --- 9 - include _includes/comps/button 10 - 11 - h1.title.is-1 404 🦀 12 - h2.subtitle.is-4 Oops! 13 - p The page you’re looking for doesn’t exist. Sorry ’bout that! :p 14 - 15 - +button("/", "_self").is-primary 16 - span Back to main page
+20
src/404.vto
··· 1 + --- 2 + layout: layouts/default.vto 3 + title: "404" 4 + doNotRenderTitle: true 5 + description: Oooops... page not found! 6 + 7 + url: /404.html 8 + --- 9 + 10 + <div class="flex flex-col text-center place-content-center place-items-center h-main-screen"> 11 + <h1 class="text-6xl font-bold mb-4">404 🦀</h1> 12 + <h2 class="text-3xl mb-4">Oops!</h2> 13 + {{ filter strip_indent |> md }} 14 + The page you're looking for doesn't exist. Sorry 'bout that! :p 15 + {{ /filter }} 16 + 17 + <a class="button rounded-full mt-8 px-10 py-3 bg-brand" href="/" target="_self"> 18 + Back to main page 19 + </a> 20 + </div>
+14 -8
src/_data.yml
··· 1 - layout: layouts/default.pug 1 + layout: layouts/default.vto 2 2 3 3 switches: 4 4 email: email-modal ··· 16 16 - text: YouTube 17 17 link: https://youtube.com/@pluiedev 18 18 icon: si-youtube 19 - color: white 19 + color: youtube 20 20 21 21 - text: pronouns.page 22 22 link: https://pronouns.page/@pluiedev 23 - icon: 24 - src: /icons/pronouns-page.svg 25 - alt: pronouns.page logo 23 + icon: /icons/pronouns-page.svg 24 + color: pronouns-page 26 25 27 - - text: Discord Server 26 + - text: Discord 28 27 link: https://discord.gg/NeNfePzCx8 29 28 icon: si-discord 30 - color: 7781f6 29 + color: discord 31 30 32 31 - text: Mastodon 33 32 link: https://blobfox.coffee/@pluie 34 33 icon: si-mastodon 35 - color: 7781f6 34 + color: mastodon 36 35 37 36 - text: Bilibili 38 37 link: https://space.bilibili.com/401096522 39 38 icon: si-bilibili 39 + color: bilibili 40 + 41 + - text: GitHub 42 + link: https://github.com/pluiedev 43 + icon: si-github 44 + color: zinc-200 45 + fg: black 40 46 41 47 technologies: 42 48 - title: I use proficiently
+23
src/_includes/comps/icon-grid.vto
··· 1 + {{ export function icon_grid(icons) }} 2 + <div class="grid grid-cols-5 gap-2"> 3 + {{ for id, lang of icons }} 4 + {{ if typeof lang === "string" }} 5 + {{ devicon(id, lang) }} 6 + {{ else }} 7 + {{> const { name, light, dark } = lang }} 8 + {{ devicon(id, name, light, dark) }} 9 + {{ /if }} 10 + {{ /for }} 11 + </div> 12 + {{ /export }} 13 + 14 + {{ function devicon(id, name, light, dark) }} 15 + {{ set parts = [id, light, dark].filter(Boolean).join("/") }} 16 + {{ set style = `"--devicon: url('https://cdn.simpleicons.org/${parts}');"` }} 17 + <div 18 + class="devicon" 19 + style={{ style }} 20 + data-tooltip={{ name }} 21 + ></div> 22 + {{ /function }} 23 +
+1 -1
src/_includes/layouts/base.pug
··· 1 1 doctype html 2 2 html(lang="en") 3 3 include ../parts/head 4 - body.pt-16(class="dark:bg-zinc-900 dark:text-zinc-200") 4 + body.pt-navbar(class="dark:bg-zinc-900 dark:text-zinc-200") 5 5 //- include ../parts/email-modal 6 6 include ../parts/navbar 7 7 block content
+11
src/_includes/layouts/base.vto
··· 1 + <!DOCTYPE html> 2 + 3 + <html lang="en"> 4 + {{ include "parts/head.vto" }} 5 + <body class="pt-navbar bg-bg text-fg"> 6 + {{ include "parts/navbar.vto" }} 7 + {{ content }} 8 + {{ include "parts/footer.vto" }} 9 + </body> 10 + </html> 11 +
+12
src/_includes/layouts/default.vto
··· 1 + {{ layout "layouts/base.vto" }} 2 + 3 + <main class="py-20 max-w-screen-lg mx-auto sm:py-10"> 4 + {{ if it.title && !it.doNotRenderTitle }} 5 + <h1 class="text-5xl font-bold my-20">{{ title }}</h1> 6 + {{ /if }} 7 + 8 + {{ content }} 9 + </main> 10 + 11 + {{ /layout }} 12 +
+2 -2
src/_includes/parts/footer.pug
··· 9 9 footer.bg-brand-darker.p-12.pb-20.text-center.prose.max-w-none.prose-invert(class="prose-a:text-blue-300 dark:prose-a:text-brand dark:bg-zinc-900 dark:border-t-2 dark:border-t-brand-dark") 10 10 p © #{yearText} Leah Amelia “pluie” Chen 11 11 :md 12 - Source code is licensed under [the Mozilla Public License](https://www.mozilla.org/MPL) 12 + Source code is licensed under the [Mozilla Public License](https://www.mozilla.org/MPL) 13 13 and available on [GitHub](https://github.com/pluiedev/site).<br/> 14 14 Content is licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). 15 15 16 - Made with [Bulma](https://bulma.io), 💜, caffeine, and estrogen pills. 16 + Made with [Lume](https://lume.land), [Tailwind CSS](https://tailwindcss.com), 💜, caffeine, and estrogen pills.
+15
src/_includes/parts/footer.vto
··· 1 + {{ set currentYear = new Date().getFullYear() }} 2 + {{ set firstYear = 2022 }} 3 + {{ set yearText = currentYear > firstYear ? `${firstYear}–${currentYear}` : `${firstYear}` }} 4 + 5 + <footer class="p-12 pb-20 text-center bg-brand-darker dark:bg-zinc-900 prose prose-invert max-w-none prose-a:text-blue-300 dark:prose-a:text-brand dark:border-t-2 dark:border-t-brand-dark"> 6 + {{ filter strip_indent |> md }} 7 + © {{yearText}} Leah Amelia “pluie” Chen 8 + 9 + Source code is licensed under the [Mozilla Public License](https://www.mozilla.org/MPL) 10 + and available on [GitHub](https://github.com/pluiedev/site).<br> 11 + Content is licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). 12 + 13 + Made with [Lume](https://lume.land), [Tailwind CSS](https://tailwindcss.com), 💜, caffeine, and estrogen pills. 14 + {{ /filter }} 15 + </footer>
+32
src/_includes/parts/head.vto
··· 1 + <head> 2 + <meta charset="utf-8" /> 3 + 4 + <title>{{it.title}} | pluie.me</title> 5 + 6 + <meta name="viewport" content="width=device-width, initial-scale=0.75" /> 7 + <meta name="description" content={{it.description}} /> 8 + <meta name="theme-color" content="#d23773" /> 9 + <meta property="og:title" content={{it.title}} /> 10 + <meta property="og:description" content={{it.description}} /> 11 + <meta property="og:url" content={{`https://pluie.me${url}`}} /> 12 + 13 + {{ for prop, val of it.opengraph }} 14 + {{ if Array.isArray(val) }} 15 + {{ for cont of val }} 16 + <meta property={{prop}} content={{cont}} /> 17 + {{ /for }} 18 + {{ else }} 19 + <meta property={{prop}} content={{val}} /> 20 + {{ /if }} 21 + {{ /for }} 22 + 23 + <link rel="preconnect" href="https://fonts.bunny.net" /> 24 + <link 25 + rel="icon" 26 + type="image/png" 27 + sizes="250x250" 28 + href="/img/avatar-small.jpg" 29 + /> 30 + 31 + <link rel="stylesheet" href="/style.css" /> 32 + </head>
+38
src/_includes/parts/navbar.vto
··· 1 + <nav class="top-0 fixed z-30 bg-navbar-bg flex h-navbar min-w-full border-b-2 border-b-navbar-border text-navbar-fg"> 2 + <input id="navbar-toggle" class="hidden" type="checkbox" aria-hidden="true" /> 3 + 4 + <a class="navbar-tab navbar-brand" href="/" title="pluie.me"></a> 5 + 6 + {{# 7 + //- label.navbar-burger(for="navbar-toggle" role="button" aria-label="menu") 8 + //- //- Three empty spans here for rendering the burger icon 9 + //- span(aria-hidden="true") 10 + //- span(aria-hidden="true") 11 + //- span(aria-hidden="true") 12 + #}} 13 + 14 + {{ for category of categories }} 15 + <a class="navbar-tab" href={{ category.url}} title={{category.title }}> 16 + {{ category.title }} 17 + </a> 18 + {{ /for }} 19 + 20 + <label class="navbar-dropdown" for={{ switches.contacts }} tabindex="0" role="button"> 21 + <input id={{ switches.contacts }} type="checkbox" class="navbar-dropdown-toggle" /> 22 + 23 + <span class="navbar-dropdown-label navbar-tab">Contact / Links</span> 24 + 25 + <div class="navbar-dropdown-items bg-white border-t-2 rounded-b-lg shadow-md shadow-black/10 dark:bg-brand-darker dark:border-t-brand-darkest"> 26 + {{ for link of links }} 27 + <a class="navbar-item flex items-center px-4 py-3 whitespace-nowrap" href={{ link.link }} target="_blank"> 28 + {{# icon #}} 29 + <span>{{ link.text }}</span> 30 + </a> 31 + {{ /for }} 32 + <label class="navbar-item flex items-center px-4 py-3 whitespace-nowrap" tabindex="0" role="button" for={{ switches.email }}> 33 + {{# icon #}} 34 + <span>Email</span> 35 + </label> 36 + </div> 37 + </label> 38 + </nav>
+2
src/_styles/components/_index.scss
··· 1 1 @import "devicon"; 2 2 //@import "display"; 3 3 //@import "dynamic-button"; 4 + @import "icon"; 4 5 //@import "langs"; 5 6 @import "link"; 6 7 //@import "message"; 7 8 @import "navbar"; 8 9 //@import "post"; 9 10 //@import "toggleable-modal"; 11 + @import "splash"; 10 12 @import "tooltip";
+1 -2
src/_styles/components/devicon.scss
··· 1 1 .devicon { 2 - @apply relative rounded-lg border-zinc-300 dark:border-zinc-700 border-2; 2 + @apply relative rounded-lg border-fg/30 border-2; 3 3 4 4 &::before { 5 5 content: ""; ··· 8 8 background: no-repeat center/75% var(--devicon); 9 9 } 10 10 } 11 -
+11
src/_styles/components/icon.scss
··· 1 + .has-icon { 2 + display: flex; 3 + align-items: center; 4 + column-gap: 0.5rem; 5 + 6 + &::before { 7 + @apply inline-block h-3/4 aspect-square; 8 + content: ""; 9 + background-image: var(--icon); 10 + } 11 + }
+1 -1
src/_styles/components/link.scss
··· 1 1 @use "sass:color"; 2 2 3 - a:not(.navbar-tab, [disabled], .pagination-link) { 3 + a:not(.button, .navbar-tab, [disabled], .pagination-link) { 4 4 // adapted from https://stackoverflow.com/a/72459455 5 5 @extend .bg-transition; 6 6 @apply text-brand ease-out bg-no-repeat bg-right-bottom bg-gradient-to-r from-transparent to-brand-dark to-0%;
+1 -6
src/_styles/components/navbar.scss
··· 13 13 // } 14 14 //} 15 15 16 - .navbar { 17 - @apply top-0 fixed z-30 bg-brand flex justify-center h-navbar min-w-full border-b-2 border-b-brand-dark text-white dark:bg-zinc-900 dark:border-b-brand; 18 - } 19 - 20 16 .navbar-brand { 21 - @extend .navbar-tab; 22 17 @apply w-32; 23 18 24 19 background: url('/icons/wordmark.svg') no-repeat center, linear-gradient(to top, var(--tw-gradient-stops)) no-repeat bottom !important; ··· 27 22 28 23 .navbar-tab { 29 24 @extend .bg-transition; 30 - @apply inline-flex h-full items-center px-3 ease-out bg-no-repeat bg-bottom bg-gradient-to-t from-transparent to-brand-dark dark:to-brand-darker to-0%; 25 + @apply inline-flex h-full items-center px-3 ease-out text-navbar-fg bg-no-repeat bg-bottom bg-gradient-to-t from-transparent to-navbar-border to-0%; 31 26 32 27 background-size: auto calc(var(--bg-transition) * 100%); 33 28 }
+44
src/_styles/components/splash.scss
··· 1 + // Like that fancy expanding/contracting splash screen I have in the main page? 2 + // Now you can have it too! Just uh... this is not really that adaptable 3 + // Good luck!!! 4 + // JavaScript *is* required. There's no way you can do this in CSS without some 5 + // seriously evil crimes 6 + 7 + .splash { 8 + padding-top: 0; 9 + transition: padding 0.5s ease-out; 10 + 11 + .intro { 12 + padding: calc(50vh - 3 * 4rem) 0; 13 + transition: padding 0.5s ease-out; 14 + } 15 + 16 + &.compact { 17 + padding-top: calc(2.5rem + 3 * 4rem); 18 + .intro { 19 + padding: 0; 20 + } 21 + } 22 + 23 + .scroll-down { 24 + @apply text-fg-dimmed transition absolute; 25 + bottom: 3rem; 26 + animation: 0.75s infinite alternate bob; 27 + 28 + &::after { 29 + @apply mx-auto block h-6 w-6 border-2 border-fg rounded-sm border-l-0 border-t-0 origin-center; 30 + transform: scale(1.5, 1.25) rotate(45deg); 31 + content: ""; 32 + } 33 + } 34 + .bottom-detector { 35 + position: absolute; 36 + bottom: -5rem; 37 + } 38 + } 39 + 40 + @keyframes bob { 41 + from { transform: translateY(0px); } 42 + to { transform: translateY(10px); } 43 + } 44 +
+1 -1
src/_styles/components/tooltip.scss
··· 3 3 4 4 &::after { 5 5 content: attr(data-tooltip); 6 - @apply hidden absolute text-sm text-center inline-block align-middle bg-white/90 dark:bg-zinc-900/80; 6 + @apply hidden absolute text-sm text-center inline-block align-middle bg-bg/80; 7 7 @apply p-1 rounded-md border-[1px] border-brand min-w-fit -inset-x-2 bottom-[90%] z-20; 8 8 } 9 9 &:hover {
+1 -1
src/assets/icons/pronouns-page.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 650"><path d="M396.52 174.35c1.35-2.4.21-4.35-2.54-4.35l-48.2.03c-2.75 0-6.15 1.94-7.54 4.31l-118.1 199.77c-16.48 27.15-39.48 33.15-61.58 30.47-37.94-4.6-58.34-32.45-58.34-69.54 0-37.25 30.31-67.56 67.56-67.56h75c2.75 0 6.12-1.95 7.48-4.34l27.03-47.2c1.37-2.39.23-4.34-2.52-4.34h-107c-68.06 0-123.44 55.37-123.44 123.44 0 32.89 12.85 68.36 36.22 91.54 23.03 22.84 53.8 31.21 86.64 31.89 18.54.21 69.46-.21 93.33-42.68 26.73-47.57 136-241.44 136-241.44zM571.94 244.44c-23.03-22.84-53.8-31.21-86.64-31.89-18.54-.21-69.46.21-93.33 42.68-26.72 47.55-136 241.42-136 241.42-1.35 2.4-.21 4.35 2.54 4.35l48.2-.03c2.75 0 6.15-1.94 7.54-4.31l118.1-199.77c16.48-27.15 39.48-33.15 61.58-30.47 37.94 4.6 58.34 32.45 58.34 69.54 0 37.25-30.31 67.56-67.56 67.56h-75c-2.75 0-6.12 1.95-7.48 4.34l-27.03 47.2c-1.37 2.39-.23 4.34 2.52 4.34h107c68.06 0 123.44-55.37 123.44-123.44 0-32.87-12.85-68.34-36.22-91.52z" fill="#c71585"/></svg> 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 650"><path d="M396.52 174.35c1.35-2.4.21-4.35-2.54-4.35l-48.2.03c-2.75 0-6.15 1.94-7.54 4.31l-118.1 199.77c-16.48 27.15-39.48 33.15-61.58 30.47-37.94-4.6-58.34-32.45-58.34-69.54 0-37.25 30.31-67.56 67.56-67.56h75c2.75 0 6.12-1.95 7.48-4.34l27.03-47.2c1.37-2.39.23-4.34-2.52-4.34h-107c-68.06 0-123.44 55.37-123.44 123.44 0 32.89 12.85 68.36 36.22 91.54 23.03 22.84 53.8 31.21 86.64 31.89 18.54.21 69.46-.21 93.33-42.68 26.73-47.57 136-241.44 136-241.44zM571.94 244.44c-23.03-22.84-53.8-31.21-86.64-31.89-18.54-.21-69.46.21-93.33 42.68-26.72 47.55-136 241.42-136 241.42-1.35 2.4-.21 4.35 2.54 4.35l48.2-.03c2.75 0 6.15-1.94 7.54-4.31l118.1-199.77c16.48-27.15 39.48-33.15 61.58-30.47 37.94 4.6 58.34 32.45 58.34 69.54 0 37.25-30.31 67.56-67.56 67.56h-75c-2.75 0-6.12 1.95-7.48 4.34l-27.03 47.2c-1.37 2.39-.23 4.34 2.52 4.34h107c68.06 0 123.44-55.37 123.44-123.44 0-32.87-12.85-68.34-36.22-91.52z" fill="#fff"/></svg>
-99
src/index.pug
··· 1 - --- 2 - title: Heya~ 3 - description: I'm Leah, also known as pluie or pluiedev! 4 - 5 - date: Git Last Modified 6 - doNotRenderTitle: true 7 - opengraph: 8 - og:type: profile 9 - profile:first_name: Leah 10 - profile:last_name: Chen 11 - profile:username: pluie 12 - profile:gender: female 13 - --- 14 - 15 - include _includes/comps/devicon.pug 16 - 17 - .grid.gap-20.mb-16(class="grid-cols-[20rem_auto_20rem] grid-rows-[calc(100vh-3*4rem)]") 18 - figure.place-self-center 19 - img.rounded-full.border-2.border-brand(src="/img/avatar.webp" alt="Avatar" width="320") 20 - .col-span-2.place-self-center 21 - h1.text-5xl.font-bold.mt-4 22 - | Heya~ I’m #[span.text-brand Leah]! 23 - span.text-xl 24 - span.mx-2 · 25 - a(href="https://pronouns.page/@pluiedev") she/her 🏳️‍⚧️ 26 - 27 - p.text-2xl.mt-4. 28 - You may know me by my online aliases, 29 - #[span.text-brand pluie] or #[span.text-brand pluiedev]. 30 - 31 - .prose.text-justify.col-span-2(class="dark:prose-invert"): :md 32 - I'm a Chinese open-source developer 👩🏼‍💻, 33 - community manager 🔨👷🏼‍♀️, 34 - graphics designer and artist 👩🏼‍🎨, 35 - dedicated to making the world of digital technology and design accessible and 36 - inclusive for everyone. 37 - 38 - I love coding, drawing, painting, discovering more about myself, and helping others discover and express themselves. 39 - Funnily enough, I do most of my productive work in my spare time when I'm supposed to relax—apparently, 40 - once I get fixated on something, I can't stop. Not sure if that's a good quality or not! ;) 41 - 42 - More things I like include dogs, traffic cones, history, politics and linguistics. 43 - Especially <abbr title="constructed languages, or languages intentionally created by people">conlangs</abbr>. 44 - They're _very_ cool. 45 - 46 - I also often think way too hard about narrative video games, to the point 47 - I write full articles and reviews about them. I'm not exactly professional, 48 - but I still try my best to figure out what does and doesn't tick! 49 - 50 - div 51 - 52 - .text-centered.w-full(class="sm:text-left") 53 - h2.text-2xl.font-bold Technologies that... 54 - 55 - .flex.divide-y.gap-4(class="sm:flex-col") 56 - each techs in technologies 57 - div 58 - h3.text-xl.my-3= techs.title 59 - +devicons(techs.icons) 60 - 61 - .prose.text-justify.col-span-2(class="dark:prose-invert") 62 - h2(class="!mb-0") Current endeavors 63 - p Up-to-date as of: #[em= filters.date(date, "HUMAN_DATE")] 64 - 65 - :md 66 - Currently, I spend most of my attention and energy on two things: 67 - community management and moderation, and contributing to and creating various 68 - open-source projects. 69 - 70 - As one of the [community managers](https://quiltmc.org/about/teams/#community-managers) 71 - of [the Quilt Project](https://quiltmc.org), a modding toolchain project primarily designed for Minecraft, 72 - I work on policies, guidelines and tooling that drives Quilt towards its goal of creating 73 - a healthy, diverse, multicultural community striving to accomodate all sorts of minorities, 74 - be they ethnic or racial minorities, sexual and/or gender-nonconfirming minorities, neurodivergent minorities, 75 - or any other group of people facing various challenges and adversities in world society. 76 - 77 - My unique identity as a queer, lesbian Chinese trans woman with an international, 78 - multicultural mindset provides me with ample amounts of lived experience with 79 - diversifying communities beyond the dominant Western, mostly Anglophone, 80 - predominantly white, cis- and heteronormative culture, 81 - which is unfortunately still the sole major cultural environment in most tech-focused communities. 82 - I hope that, through my efforts, heterogeneous people from diverse backgrounds can find 83 - communities that I manage comfortable, and be free from having to mask their cultural heritage, 84 - like when I first ventured into international technical communities. 85 - 86 - As an open-source contributor, I occasionally submit patches and pull requests to 87 - projects I find interesting, mostly in Rust, TypeScript and Kotlin. 88 - I also maintain Quilt's community tooling, mainly [its official website](https://quiltmc.org) 89 - and its [Developer Wiki](https://modder.wiki.quiltmc.org/), and sometimes its own 90 - Discord bot called [Cozy](https://github.com/QuiltMC/cozy-discord). 91 - I also started some other random projects, one of which is a 92 - [Rust rewrite of the `alien` tool](https://github.com/pluiedev/alien), 93 - originally a Perl utility script that converts Linux software packages between various formats 94 - (e.g.: `.deb`, `.rpm`, `.tgz`, ...) for different package managers (e.g.: `dpkg`/`apt`, `rpm`/`dnf`, ...) 95 - 96 - I'm almost always coding something new, for existing projects or otherwise, 97 - and I can't wait to show you more! For now, you'll just have to wait for me to update this 98 - portfolio again sometime in the future... 99 -
+177
src/index.vto
··· 1 + --- 2 + layout: layouts/base.vto 3 + title: Heya~ 4 + description: I'm Leah, also known as pluie or pluiedev! 5 + 6 + date: Git Last Modified 7 + doNotRenderTitle: true 8 + opengraph: 9 + og:type: profile 10 + profile:first_name: Leah 11 + profile:last_name: Chen 12 + profile:username: pluie 13 + profile:gender: female 14 + --- 15 + {{ import { icon_grid } from "comps/icon-grid.vto" }} 16 + 17 + <link rel="stylesheet" href="main.css" /> 18 + 19 + <div class="top-detector"></div> 20 + <main class="grid splash justify-items-center gap-x-16 gap-y-10 mx-auto max-w-screen-lg mb-16 grid-cols-main-screen"> 21 + <figure class="intro place-self-center"> 22 + <img class="rounded-full border-2 border-brand" src="/img/avatar.webp" alt="Avatar" width="320" height="320" /> 23 + </figure> 24 + 25 + <div class="intro col-span-2 place-self-center flex flex-col gap-y-4"> 26 + <h1 class="text-5xl font-bold"> 27 + Heya~ I’m <span class="text-brand">Leah</span>! 28 + <span class="text-xl"> 29 + <span class="mx-2">·</span> 30 + <a href="https://pronouns.page/@pluiedev">she/her 🏳️‍⚧️</a> 31 + </span> 32 + </h1> 33 + 34 + <p class="text-2xl"> 35 + You may know me by my online aliases, 36 + <span class="text-brand">pluie</span> or 37 + <span class="text-brand">pluiedev</span>. 38 + </p> 39 + <p class="text-xl"> 40 + I'm a Chinese open-source <span class="text-brand">developer 👩🏼‍💻</span>, 41 + community <span class="text-brand">manager 🔨👷🏼‍♀️</span>, 42 + graphics designer and <span class="text-brand">artist 👩🏼‍🎨</span>, 43 + dedicated to making the world of technology and design accessible and 44 + inclusive for <span class="text-brand">everyone</span>. 45 + </p> 46 + </div> 47 + 48 + <div class="scroll-down"> 49 + Scroll down 50 + <div class="bottom-detector"></div> 51 + </div> 52 + 53 + <div id="scroll-indicator-vanish-point" class="prose text-justify col-span-2 dark:prose-invert"> 54 + {{ filter strip_indent |> md }} 55 + I love coding, drawing, painting, discovering more about myself, and helping others discover and express themselves. 56 + Funnily enough, I do most of my productive work in my spare time when I'm supposed to relax—apparently, 57 + once I get fixated on something, I can't stop. Not sure if that's a good quality or not! ;) 58 + 59 + More things I like include dogs, traffic cones, history, politics and linguistics. 60 + Especially <abbr title="constructed languages, or languages intentionally created by people">conlangs</abbr>. 61 + They're _very_ cool. 62 + 63 + I also often think way too hard about narrative video games, to the point 64 + I write full articles and reviews about them. I'm not exactly professional, 65 + but I still try my best to figure out what does and doesn't tick! 66 + {{ /filter }} 67 + </div> 68 + 69 + <div class="flex flex-col w-full place-content-center gap-2"> 70 + {{ for link of links }} 71 + {{ set fg = link.fg ?? "white" }} 72 + {{ if link.icon.startsWith("si-") }} 73 + {{ set icon = `https://cdn.simpleicons.org/${link.icon.substring(3)}/${fg}` }} 74 + {{ else }} 75 + {{ set icon = link.icon }} 76 + {{ /if }} 77 + 78 + <a 79 + href={{ link.link }} 80 + class="button arrow-button has-icon bg-{{ link.color }} py-2 px-3 text-{{ fg }}" 81 + style="--icon: url({{ icon }})" 82 + > 83 + {{ link.text }} 84 + </a> 85 + {{ /for }} 86 + </div> 87 + 88 + <div class="text-centered w-full sm:text-left"> 89 + <h2 class="text-2xl font-bold">Technologies that...</h2> 90 + 91 + <div class="flex divide-y gap-4 sm:flex-col"> 92 + {{ for techs of technologies }} 93 + <div> 94 + <h3 class="text-xl my-3">{{ techs.title }}</h3> 95 + {{ icon_grid(techs.icons) }} 96 + </div> 97 + {{ /for }} 98 + </div> 99 + </div> 100 + 101 + <div class="prose text-justify col-span-2 dark:prose-invert"> 102 + <h2 class="!mb-0">Current endeavors</h2> 103 + <p>Up-to-date as of: <em>{{ it.date |> date("HUMAN_DATE") }}</em></p> 104 + 105 + {{ filter strip_indent |> md }} 106 + Currently, I spend most of my attention and energy on two things: 107 + community management and moderation, and contributing to and creating various 108 + open-source projects. 109 + 110 + As one of the [community managers](https://quiltmc.org/about/teams/#community-managers) 111 + of [the Quilt Project](https://quiltmc.org), a modding toolchain project primarily designed for Minecraft, 112 + I work on policies, guidelines and tooling that drives Quilt towards its goal of creating 113 + a healthy, diverse, multicultural community striving to accomodate all sorts of minorities, 114 + be they ethnic or racial minorities, sexual and/or gender-nonconfirming minorities, neurodivergent minorities, 115 + or any other group of people facing various challenges and adversities in world society. 116 + 117 + My unique identity as a queer, lesbian Chinese trans woman with an international, 118 + multicultural mindset provides me with ample amounts of lived experience with 119 + diversifying communities beyond the dominant Western, mostly Anglophone, 120 + predominantly white, cis- and heteronormative culture, 121 + which is unfortunately still the sole major cultural environment in most tech-focused communities. 122 + I hope that, through my efforts, heterogeneous people from diverse backgrounds can find 123 + communities that I manage comfortable, and be free from having to mask their cultural heritage, 124 + like when I first ventured into international technical communities. 125 + 126 + As an open-source contributor, I occasionally submit patches and pull requests to 127 + projects I find interesting, mostly in Rust, TypeScript and Kotlin. 128 + I also maintain Quilt's community tooling, mainly [its official website](https://quiltmc.org) 129 + and its [Developer Wiki](https://modder.wiki.quiltmc.org/), and sometimes its own 130 + Discord bot called [Cozy](https://github.com/QuiltMC/cozy-discord). 131 + I also started some other random projects, one of which is a 132 + [Rust rewrite of the \`alien\` tool](https://github.com/pluiedev/alien), 133 + originally a Perl utility script that converts Linux software packages between various formats 134 + (e.g.: \`.deb\`, \`.rpm\`, \`.tgz\`, ...) for different package managers (e.g.: \`dpkg\`/\`apt\`, \`rpm\`/\`dnf\`, ...) 135 + 136 + I'm almost always coding something new, for existing projects or otherwise, 137 + and I can't wait to show you more! For now, you'll just have to wait for me to update this 138 + portfolio again sometime in the future... 139 + {{ /filter }} 140 + </div> 141 + </main> 142 + 143 + <script> 144 + const scroll = document.querySelector('.scroll-down'); 145 + const intro = document.querySelector('main'); 146 + 147 + const t = new IntersectionObserver(entries => { 148 + entries.forEach(entry => { 149 + if (entry.isIntersecting) { 150 + scroll.classList.remove('opacity-0'); 151 + intro.classList.remove('compact'); 152 + } 153 + }); 154 + }); 155 + const b = new IntersectionObserver(entries => { 156 + entries.forEach(entry => { 157 + if (entry.isIntersecting) { 158 + scroll.classList.add('opacity-0'); 159 + intro.classList.add('compact'); 160 + } 161 + }); 162 + }); 163 + 164 + function init(ev) { 165 + if (window.innerHeight < 600) { 166 + scroll.classList.add('opacity-0'); 167 + t.disconnect(); 168 + b.disconnect(); 169 + } else { 170 + t.observe(document.querySelector('.top-detector')); 171 + b.observe(document.querySelector('.bottom-detector')); 172 + } 173 + } 174 + 175 + window.addEventListener('resize', init) 176 + window.addEventListener('load', init) 177 + </script>
+14
src/main.scss
··· 1 + .arrow-button { 2 + @apply relative; 3 + 4 + &::after { 5 + @apply absolute w-0 h-0 right-0 border-bg border-l-transparent; 6 + content: ""; 7 + transition: border-width 0.25s; 8 + border-width: 1.25rem 1.5rem 1.25rem 1rem; 9 + } 10 + 11 + &:hover::after { 12 + border-right-width: 0rem; 13 + } 14 + }
+29
src/style.scss
··· 2 2 @tailwind components; 3 3 @tailwind utilities; 4 4 5 + :root { 6 + // Default dark theme "Neon" 7 + 8 + --brand: 210 55 115; // #d23773 9 + --brand-dark: 186 44 99; // #ba2c63 10 + --brand-darker: 133 30 70; // #851e46 11 + --brand-darkest: 87 18 45; // #57122d 12 + 13 + --bg: 24 24 27; // zinc-900 14 + 15 + --navbar-bg: var(--bg); 16 + --navbar-fg: 255 255 255; // white 17 + --navbar-border: var(--brand); 18 + 19 + --fg: 212 212 216; // zinc-200 20 + } 21 + @media (prefers-color-scheme: light) { 22 + :root { 23 + // Default light theme "Strawberry Marshmellow" 24 + 25 + --bg: 255 255 255; // white 26 + --navbar-bg: var(--brand); 27 + --navbar-border: var(--brand-dark); 28 + 29 + --fg: 24 24 27; // zinc-900 30 + --fg-dimmed: 63 63 70; // zinc-700 31 + } 32 + } 33 + 5 34 @layer base { 6 35 } 7 36
+120 -57
src/works.pug src/works.vto
··· 1 1 --- 2 2 title: Works 3 3 description: Projects, artworks, and other things I've done... 4 + 5 + artworks: 6 + - id: tiger 7 + name: Ferality 8 + imgid: 558e95a4-f685-4dee-32a6-8c33e49d9600 9 + size: medium 10 + aspect_ratio: square 11 + width: 400 12 + height: 408 13 + 14 + - id: blossoms 15 + name: Altitudinous blossoms 16 + imgid: 170696fd-d9b4-4652-e168-2cc80250d400 17 + size: medium 18 + aspect_ratio: 3by5 19 + width: 400 20 + height: 660 21 + 22 + - id: fish 23 + name: A piscine aura in the depths 24 + imgid: 278a69c6-ec0a-4461-c3f3-79aa615ad800 25 + size: large 26 + aspect_ratio: 4by3 27 + width: 500 28 + height: 380 29 + 30 + - id: red_cotton 31 + name: A flower — what's her name again? 32 + imgid: 35db3ba1-542a-46aa-2d10-850d34463e00" 33 + size: large 34 + aspect_ratio: 4by3 35 + rtl: true 36 + width: 500 37 + height: 372 38 + 39 + - id: laotie 40 + name: 老铁 (Laotie) 41 + imgid: d35a98e6-9215-4ca7-ac79-133c93d17700 42 + size: medium 43 + aspect_ratio: 3by4 44 + width: 400 45 + height: 533 46 + 47 + - id: mom_and_i 48 + name: By the lake, beneath the canopies, in the beforetimes 49 + imgid: 34e6b9ba-08a0-4a1a-437c-2e8839591500 50 + size: medium 51 + aspect_ratio: 3by4 52 + width: 400 53 + height: 515 4 54 --- 5 55 56 + {{# 6 57 include _includes/comps/icon 7 58 include _includes/comps/modal 59 + #}} 8 60 9 - h2.title.is-3 Projects 61 + <h2 class="text-3xl font-bold">Projects</h2> 10 62 11 - :md 63 + {{ filter strip_indent |> md }} 12 64 Under construction! :purple_heart: 13 65 (Yeah, I know, it's been ages, gimme some more time) 66 + {{ /filter }} 14 67 15 - h2.title.is-3 Artworks 68 + <h2 class="text-3xl font-bold">Artworks</h2> 16 69 17 - mixin display(id, title, imgid) 18 - -const { height, width, aspectRatio, ...rest } = attributes; 70 + {{ function display(attrs) }} 71 + {{> const { id, title, imgid, height, width, aspect_ratio } = attrs }} 19 72 20 - figure.display&attributes(rest) 21 - label(for=id role="button" tabindex="0") 22 - img( 23 - src=`https://imagedelivery.net/TLP_u-wyyvTEPKkgbA6Osg/${imgid}/public` 24 - alt=title 25 - width=width 26 - height=height 27 - ) 28 - 29 - figcaption 30 - h3.title.is-4= title 31 - block 73 + <figure class="display"> 74 + <label for={{ id }} role="button" tabindex="0"> 75 + <img 76 + src="https://imagedelivery.net/TLP_u-wyyvTEPKkgbA6Osg/{{ imgid }}/public" 77 + alt={{ title }} 78 + width={{ width }} 79 + height={{ height }} 80 + /> 81 + </label> 82 + 83 + <figcaption> 84 + <h3 class="text-2xl font-bold">{{ title }}</h3> 85 + {{ it[id] }} 86 + </figcaption> 87 + {{# 32 88 +modal(id) 33 89 .image.is-fullwidth(class=aspectRatio) 34 90 img( ··· 37 93 fetchpriority="low" 38 94 loading="lazy" 39 95 ) 96 + #}} 97 + {{ /function }} 40 98 41 - mixin header(date, medium) 42 - p.subtitle.is-6 43 - span.icon-text 44 - +icon("far fa-calendar") 45 - span= filters.date(date, "PP") 46 - +icon("fas fa-brush") 47 - span= medium 99 + {{ function header(date, medium) }} 100 + <p class="text-xl"> 101 + <i data-lucide="calendar"></i> 102 + {{ date |> date("PP") }} 103 + <i data-lucide="brush"></i> 104 + {{ medium }} 105 + </p> 106 + {{ /function }} 48 107 49 - +display("tiger", "Ferality", "558e95a4-f685-4dee-32a6-8c33e49d9600").has-frame( 50 - size="medium" aspectRatio="is-square" width=400 height=408 51 - ) 52 - +header("2018-12-02", "Oil on canvas") 53 - :md 108 + {{ set tiger }} 109 + {{ header("2018-12-02", "Oil on canvas") }} 110 + {{ filter strip_indent |> md }} 54 111 One of the older works of mine — I think I'd only spent 2 years dabbling with oil paintings back then, 55 112 and the technical inexpertise definitely shows — but a brazen and unpolished technique does not 56 113 necessarily hinder expressiveness. ;) 57 - 114 + 58 115 I really wanted to capture the anger in the tiger, and as I exaggerated its features and started furiously 59 116 laying down sweeping strokes of striking orange, I too experienced the feral fury in me. 60 117 Wish I could experience that again... 118 + {{ /filter }} 119 + {{ /set }} 61 120 62 - +display("altitudinous_blossoms", "Altitudinous blossoms", "170696fd-d9b4-4652-e168-2cc80250d400").has-frame( 63 - size="medium" aspectRatio="is-3by5" rtl width=400 height=660 64 - ) 65 - +header("2021-02-11", "Oil on canvas") 66 - :md 121 + {{ set blossoms }} 122 + {{ header("2021-02-11", "Oil on canvas") }} 123 + {{ filter strip_indent |> md }} 67 124 A friend of my mother's is a hobbyist photographer, and I think he took the original photo when he went to Tibet to... 68 125 well, take pictures of the wild flora and fauna and the majestic landscape. 69 126 (Y'know, like a photographer? Why else would he be there anyway?) ··· 76 133 I did take a while to figure it out though. I also used a toothbrush to ~~paint~~ *spray* the white dots on, 77 134 which are actually specks of mint-scented toothpaste and not white paint! 78 135 Although, they look like they hold up just fine on the canvas. 136 + {{ /filter }} 137 + {{ /set }} 79 138 80 - +display("tropical_fish", "A piscine aura in the depths", "278a69c6-ec0a-4461-c3f3-79aa615ad800").has-frame( 81 - size="large" aspectRatio="is-4by3" width=500 height=380 82 - ) 83 - +header("2021-06-05", "Oil on canvas") 84 - :md 139 + {{ set fish }} 140 + {{ header("2021-06-05", "Oil on canvas") }} 141 + {{ filter strip_indent |> md }} 85 142 Some kind of tropical fish whose name I have completely forgotten. It didn't even occur to past-Leah to write these things down! *sigh* 86 143 87 144 It's really pretty, especially for its lake-blue scales and blood-red fins! Sure did take a long time to paint though... It was around this time 88 145 when my style really departed from the raw, emotional expression like *Ferality*, towards a finer sort of aesthetic. 146 + {{ /filter }} 147 + {{ /set }} 89 148 90 - +display("red_cotton", "A flower — what's her name again?", "35db3ba1-542a-46aa-2d10-850d34463e00").has-frame( 91 - size="large" aspectRatio="is-4by3" rtl width=500 height=372 92 - ) 93 - +header("2021-12-11", "Oil on canvas") 94 - :md 149 + {{ set red_cotton }} 150 + {{ header("2021-12-11", "Oil on canvas") }} 151 + {{ filter strip_indent |> md }} 95 152 Turns out it's a flower of the tree [*Bombax ceiba*](https://en.wikipedia.org/wiki/Bombax_ceiba), better known as 96 153 the red cotton tree, or <ruby>木棉 <rp>(</rp><rt>mù mián</rt><rp>)</rp></ruby> in Chinese. 97 154 I was going to leave it unidentified, but my mother's a bonafide botanist and identified it for me. ··· 99 156 Its striking red color is honestly captivating, and I still have it displayed on the wall of my room, 100 157 facing the entrance for I hope any visitors would appreciate its vividity and life force seeping out of the canvas, 101 158 as much as I would. 159 + {{ /filter }} 160 + {{ /set }} 102 161 103 - +display("laotie", "老铁 (Laotie)", "d35a98e6-9215-4ca7-ac79-133c93d17700").has-frame( 104 - size="medium" aspectRatio="is-3by4" width=400 height=533 105 - ) 106 - +header("2022-03-20", "Oil on canvas") 107 - :md 162 + {{ set laotie }} 163 + {{ header("2022-03-20", "Oil on canvas") }} 164 + {{ filter strip_indent |> md }} 108 165 He's a corgi in the studio we used to paint in, and his presence made most of our weekly painting hours turn into communal dog-petting sessions. 109 166 It was lovely to paint with such an adorable cutie around, and time and stress all went away so swiftly with him. 110 167 I still have like, *so* many photos of him playing around with us... Too bad that that studio was shut down about 2 months ago, on February 2023. 111 168 112 169 My mother insists I painted Santa Claus in disguise of a dog! But really, he's just wearing some festive clothes... 170 + {{ /filter }} 171 + {{ /set }} 113 172 114 - +display("mom_and_i", "By the lake, beneath the canopies, in the beforetimes", "34e6b9ba-08a0-4a1a-437c-2e8839591500").has-frame( 115 - size="medium" aspectRatio="is-3by4" width=400 height=515 116 - ) 117 - +header("2022-07-23", "Oil on canvas") 118 - :md 173 + {{ set mom_and_i }} 174 + {{ header("2022-07-23", "Oil on canvas") }} 175 + {{ filter strip_indent |> md }} 119 176 One summer afternoon, on the eastern shore of Jinji Lake, Suzhou. It was an awfully nice day, so my mother and I decided 120 177 to take a selfie on our daily stroll outside. 121 178 Originally, I decided to paint this as a work of memorabilia, to make me cherish the good times I spent with my mother at home, ··· 123 180 124 181 But now, it has taken an additional level of meaning — this was the last painting I finished before I realized I was a trans woman. 125 182 That tall silhouette with the masculine build was supposed to be me, but now it's representative of the person I used to be. 126 - 183 + 127 184 However, I have to remain truthful to myself — regardless of who I am now, I *was* like that back then, and this fossilized image 128 - of me is one that I can't, and don't want to change. The past is not to be changed — the future, however, is. 185 + of me is one that I can't, and don't want to change. The past is not to be changed — the future, however, is. 186 + {{ /filter }} 187 + {{ /set }} 188 + 189 + {{ for artwork of artworks }} 190 + {{ display(artwork) }} 191 + {{ /for }}
+32 -20
tailwind.config.ts
··· 1 1 import typography from "npm:@tailwindcss/typography"; 2 2 3 + const themeColors = [ 4 + "brand", 5 + "brand-dark", 6 + "brand-darker", 7 + "brand-darkest", 8 + "fg", 9 + "fg-dimmed", 10 + "navbar-bg", 11 + "navbar-fg", 12 + "navbar-border", 13 + "bg", 14 + "bg-deemphasized", 15 + ]; 16 + 17 + const colors = { 18 + youtube: "#d02525", 19 + bilibili: "#00a1d6", 20 + github: "#fafafa", 21 + "pronouns-page": "#c71585", 22 + discord: "#5865f2", 23 + mastodon: "#6364ff", 24 + }; 25 + for (const themeColor of themeColors) { 26 + colors[themeColor] = `rgba(var(--${themeColor}) / <alpha-value>)`; 27 + } 28 + 3 29 export default { 4 30 theme: { 5 31 extend: { 6 - colors: { 7 - brand: "#d23773", 8 - "brand-dark": "#ba2c63", 9 - "brand-darker": "#851e46", 10 - "brand-darkest": "#57122d", 11 - 12 - youtube: "#d02525", 13 - twitter: "#0077d6", 14 - github: "#fafafa", 15 - "pronouns-page": "#c71585", 16 - blurple: "#5865f2", 17 - }, 18 - backgroundSize: { 19 - wordmark: "75%", 32 + colors, 33 + spacing: { 34 + navbar: "4rem", 20 35 }, 21 - backgroundImage: { 22 - wordmark: "url('/icons/wordmark.svg')", 36 + gridTemplateColumns: { 37 + "main-screen": "20rem auto 20rem", 23 38 }, 24 39 height: { 25 - navbar: "4rem", 26 - }, 27 - minHeight: { 28 - "main-screen": "calc(100vh - 2 * 4rem)", 40 + "main-screen": "calc(100vh - 4 * 4rem)", 29 41 }, 30 42 }, 31 43 fontFamily: {
+116
vento-improved.ts
··· 1 + import { defaults, Options } from "lume/plugins/vento.ts"; 2 + import engine from "https://deno.land/x/vento@v0.7.1/mod.ts"; 3 + import { Environment } from "https://deno.land/x/vento@v0.7.1/src/environment.ts"; 4 + import { FileLoader } from "https://deno.land/x/vento@v0.7.1/src/loader.ts"; 5 + 6 + import { Data, Engine, FS, Helper, Site } from "lume/core.ts"; 7 + import loader from "lume/core/loaders/text.ts"; 8 + import { merge, normalizePath } from "lume/core/utils.ts"; 9 + 10 + export type Tag = ( 11 + env: Environment, 12 + code: string, 13 + output: string, 14 + tokens: Token[] 15 + ) => string | undefined; 16 + 17 + class LumeLoader extends FileLoader { 18 + fs: FS; 19 + constructor(includes: string, fs: FS) { 20 + super(includes); 21 + this.fs = fs; 22 + } 23 + async load(file: string) { 24 + const entry = this.fs.entries.get(normalizePath(file)); 25 + if (!entry) { 26 + throw new Error(`File not found: ${file}`); 27 + } 28 + const data = await entry.getContent(loader); 29 + return { source: data.content as string, data }; 30 + } 31 + } 32 + 33 + /** Template engine to render Vento files */ 34 + export class VentoEngine implements Engine { 35 + engine: Environment; 36 + constructor(engine: Environment) { 37 + this.engine = engine; 38 + } 39 + deleteCache(file: string) { 40 + this.engine.cache.delete(file); 41 + } 42 + render(content: string, data: Data = {}, filename?: string) { 43 + return this.engine 44 + .runString(content, data, filename) 45 + .then((m) => m.content); 46 + } 47 + renderSync(content: string, data: Data = {}, filename?: string): string { 48 + return this.engine.runStringSync(content, data, filename).content; 49 + } 50 + 51 + addHelper(name: string, fn: Helper) { 52 + this.engine.filters[name] = fn; 53 + } 54 + } 55 + 56 + export default function (userOptions?: Partial<Options>) { 57 + const options = merge(defaults, userOptions); 58 + const extensions = Array.isArray(options.extensions) 59 + ? { pages: options.extensions, components: options.extensions } 60 + : options.extensions; 61 + 62 + return (site: Site) => { 63 + const env = engine({ 64 + includes: new LumeLoader(normalizePath(site.options.includes), site.fs), 65 + dataVarname: options.options.dataVarname, 66 + }); 67 + 68 + const patch = { 69 + async load(file: string, from?: string): Promise<Template> { 70 + const path = from ? this.options.loader.resolve(from, file) : file; 71 + 72 + if (!this.cache.has(path)) { 73 + const { source, data } = await this.options.loader.load(path); 74 + const template = this.compile(source, path, data); 75 + 76 + // BUGFIX: use `path`, not `file` 77 + this.cache.set(path, template); 78 + } 79 + 80 + // BUGFIX: use `path`, not `file` 81 + return this.cache.get(path)!; 82 + }, 83 + }; 84 + 85 + const vento = Object.assign(env, patch); 86 + vento.tags.push(filterTag); 87 + 88 + const ventoEngine = new VentoEngine(vento); 89 + 90 + site.loadPages(extensions.pages, loader, ventoEngine); 91 + site.loadComponents(extensions.components, loader, ventoEngine); 92 + }; 93 + } 94 + 95 + const filterTag = (env, code, output, tokens) => { 96 + const match = code.match(/^filter (.*)$/); 97 + if (!match) return; 98 + const [_, filter] = match; 99 + tokens.unshift(["filter", filter]); 100 + 101 + const varname = "__content"; 102 + const filters = env.compileFilters(tokens, varname); 103 + const content = env.compileTokens(tokens, varname, ["/filter"]); 104 + 105 + const tok = tokens.shift(); 106 + if (tok && (tok[0] !== "tag" || tok[1] !== "/filter")) { 107 + throw new Error(`Missing closing tag for filter '${filter.name}': ${code}`); 108 + } 109 + 110 + const res = [ 111 + `{let ${varname} = "";`, 112 + ...content, 113 + `${output} += ${filters};}`, 114 + ].join("\n"); 115 + return res; 116 + };