Initial commit
@@ -2,9 +2,46 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
import svelte from '@astrojs/svelte';
|
||||
import react from '@astrojs/react';
|
||||
import darkPlus from 'shiki/themes/dark-plus.mjs'
|
||||
|
||||
import remarkToc from 'remark-toc'
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://example.com',
|
||||
integrations: [mdx(), sitemap()],
|
||||
site: 'https://www.lbfalvy.com',
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
},
|
||||
integrations: [mdx({
|
||||
shikiConfig: {
|
||||
theme: {
|
||||
...darkPlus,
|
||||
bg: "var(--color-side-bg)"
|
||||
}
|
||||
},
|
||||
remarkPlugins: [
|
||||
[remarkToc, {
|
||||
tight: true,
|
||||
skip: '.{0}' // Nothing
|
||||
}]
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypeAutolinkHeadings, {
|
||||
content: {
|
||||
type: 'element',
|
||||
tagName: 'i',
|
||||
properties: {
|
||||
className: 'linkbtn gg-link'
|
||||
}
|
||||
}
|
||||
}]
|
||||
]
|
||||
}), sitemap(), svelte(), react()],
|
||||
});
|
||||
1674
package-lock.json
generated
13
package.json
@@ -10,8 +10,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.0.7",
|
||||
"@astrojs/react": "^4.1.6",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"astro": "^5.1.9"
|
||||
"@astrojs/svelte": "^7.0.4",
|
||||
"@js-temporal/polyfill": "^0.4.4",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"astro": "^5.1.9",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"shiki": "^2.1.0",
|
||||
"svelte": "^5.19.3",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 749 B |
@@ -1,55 +0,0 @@
|
||||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import '../styles/global.css';
|
||||
import { SITE_TITLE } from '../consts';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={SITE_TITLE}
|
||||
href={new URL('rss.xml', Astro.site)}
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Font preloads -->
|
||||
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
||||
83
src/components/BlogList.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { onMount } from "svelte";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
import { parseTime, printTime } from "../utils/time";
|
||||
|
||||
interface Props {
|
||||
posts: CollectionEntry<"blog">[];
|
||||
}
|
||||
let { posts }: Props = $props();
|
||||
|
||||
let allTags = $derived([...new Set(posts.flatMap(item => item.data.tags))]);
|
||||
|
||||
let selectedTags = $state<SvelteMap<string, boolean>>();
|
||||
|
||||
onMount(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const tagsQry = url.searchParams.get('tags');
|
||||
const defaultTags = tagsQry ? new Set(decodeURIComponent(tagsQry).split('+')) : new Set();
|
||||
selectedTags = new SvelteMap(allTags.map(tag => [tag, defaultTags.has(tag)]));
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!selectedTags) return;
|
||||
const newTagsQry = encodeURIComponent(
|
||||
[...selectedTags.entries()].filter(([_, v]) => v).map(([k, _]) => k).join('+')
|
||||
);
|
||||
const url = new URL(window.location.href);
|
||||
if (newTagsQry == url.searchParams.get("tags")) return;
|
||||
if (newTagsQry == "") url.searchParams.delete("tags");
|
||||
else url.searchParams.set("tags", newTagsQry);
|
||||
window.history.pushState(null, "", url.toString());
|
||||
})
|
||||
|
||||
let isFiltered = $derived(selectedTags && [...selectedTags.values()].includes(true));
|
||||
let filteredPosts = $derived(isFiltered
|
||||
? posts.filter(p => !p.data.unlisted && p.data.tags.some(t => selectedTags!.get(t)))
|
||||
: posts.filter(p => !p.data.unlisted));
|
||||
let shownPosts = $derived(filteredPosts.toReversed())
|
||||
</script>
|
||||
|
||||
<div class="lg:flex flex-row-reverse justify-between items-start">
|
||||
<div class="m-2 p-2 bg-side-bg lg:float-right lg:max-w-80 lg:mr-4">
|
||||
<span class="inline-block m-0.5 pl-2 italic">filter:</span>
|
||||
{#each allTags as tag}
|
||||
<button
|
||||
onclick={() => {
|
||||
console.log("TRoggled tag")
|
||||
selectedTags!.set(tag, !selectedTags!.get(tag))
|
||||
}}
|
||||
class={[
|
||||
"m-0.5 rounded-4xl emph-bg px-2 cursor-pointer border-2 border-solid",
|
||||
selectedTags?.get(tag) && "border-gray-400"
|
||||
]}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<main class="max-w-[650px]">
|
||||
{#each shownPosts as post}
|
||||
<article>
|
||||
<a href={`/blog/${post.id}`}>
|
||||
<div class="
|
||||
lg:grid grid-cols-[auto_min-content_min-content]
|
||||
m-1 p-2 hover:bg-shade
|
||||
">
|
||||
<h2 class="font-bold">{post.data.title}</h2>
|
||||
<address class="inline-block post-meta col-start-2 md:ml-3">
|
||||
{post.data.author}
|
||||
</address>
|
||||
<time class="inline-block post-meta col-start-3 lg:ml-1"
|
||||
datetime={parseTime(post.data.pubDate).toString()}
|
||||
>
|
||||
{printTime(parseTime(post.data.pubDate))}
|
||||
</time>
|
||||
<div class="col-span-3">{post.data.summary}</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
{/each}
|
||||
</main>
|
||||
</div>
|
||||
4
src/components/CodeError.astro
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
const { text } = Astro.props;
|
||||
---
|
||||
<pre class="text-red-300">{text?.trim()}<slot/></pre>
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
const today = new Date();
|
||||
---
|
||||
|
||||
<footer>
|
||||
© {today.getFullYear()} Your name here. All rights reserved.
|
||||
<div class="social-links">
|
||||
<a href="https://m.webtoo.ls/@astro" target="_blank">
|
||||
<span class="sr-only">Follow Astro on Mastodon</span>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
width="32"
|
||||
height="32"
|
||||
astro-icon="social/mastodon"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
<a href="https://twitter.com/astrodotbuild" target="_blank">
|
||||
<span class="sr-only">Follow Astro on Twitter</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/twitter"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
<a href="https://github.com/withastro/astro" target="_blank">
|
||||
<span class="sr-only">Go to Astro's GitHub repo</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
<style>
|
||||
footer {
|
||||
padding: 2em 1em 6em 1em;
|
||||
background: linear-gradient(var(--gray-gradient)) no-repeat;
|
||||
color: rgb(var(--gray));
|
||||
text-align: center;
|
||||
}
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.social-links a {
|
||||
text-decoration: none;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
.social-links a:hover {
|
||||
color: rgb(var(--gray-dark));
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
interface Props {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
const { date } = Astro.props;
|
||||
---
|
||||
|
||||
<time datetime={date.toISOString()}>
|
||||
{
|
||||
date.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
</time>
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
import HeaderLink from './HeaderLink.astro';
|
||||
import { SITE_TITLE } from '../consts';
|
||||
---
|
||||
|
||||
<header>
|
||||
<nav>
|
||||
<h2><a href="/">{SITE_TITLE}</a></h2>
|
||||
<div class="internal-links">
|
||||
<HeaderLink href="/">Home</HeaderLink>
|
||||
<HeaderLink href="/blog">Blog</HeaderLink>
|
||||
<HeaderLink href="/about">About</HeaderLink>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a href="https://m.webtoo.ls/@astro" target="_blank">
|
||||
<span class="sr-only">Follow Astro on Mastodon</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
<a href="https://twitter.com/astrodotbuild" target="_blank">
|
||||
<span class="sr-only">Follow Astro on Twitter</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
<a href="https://github.com/withastro/astro" target="_blank">
|
||||
<span class="sr-only">Go to Astro's GitHub repo</span>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<style>
|
||||
header {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(var(--black), 5%);
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h2 a,
|
||||
h2 a.active {
|
||||
text-decoration: none;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
nav a {
|
||||
padding: 1em 0.5em;
|
||||
color: var(--black);
|
||||
border-bottom: 4px solid transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
nav a.active {
|
||||
text-decoration: none;
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
.social-links,
|
||||
.social-links a {
|
||||
display: flex;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.social-links {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
|
||||
type Props = HTMLAttributes<'a'>;
|
||||
|
||||
const { href, class: className, ...props } = Astro.props;
|
||||
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
|
||||
const subpath = pathname.match(/[^\/]+/g);
|
||||
const isActive = href === pathname || href === '/' + (subpath?.[0] || '');
|
||||
---
|
||||
|
||||
<a href={href} class:list={[className, { active: isActive }]} {...props}>
|
||||
<slot />
|
||||
</a>
|
||||
<style>
|
||||
a {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.active {
|
||||
font-weight: bolder;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
160
src/components/LambdaHighlight.astro
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
import type { JSX } from "astro/jsx-runtime"
|
||||
|
||||
type LambdaToken = ['lambda', string, Token[], Token[]]
|
||||
type Token =
|
||||
| ['comment', string]
|
||||
| LambdaToken
|
||||
| ['operator', string]
|
||||
| ['name', string]
|
||||
| ['whitespace', string]
|
||||
| ['placeholder', string]
|
||||
| ['macro', string]
|
||||
| ['keyword', string]
|
||||
| ['string', string]
|
||||
| ['number', string]
|
||||
|
||||
function matchParen(expr: string, greedy = false): string {
|
||||
for (var i = 0, lvl = 0;
|
||||
0 <= lvl && i < expr.length;
|
||||
i++) {
|
||||
if (expr[i] == '(' || expr[i] == '\\') lvl++
|
||||
else if (expr[i] == ')' || expr[i] == '.') lvl--
|
||||
else if (greedy && expr[i] == '\n' && lvl == 0) break
|
||||
}
|
||||
return expr.slice(0, i)
|
||||
}
|
||||
|
||||
function parseLambda(expr: string): LambdaToken {
|
||||
expr = expr.slice(1) // Get rid of \
|
||||
const nameMatch = /^[\$a-zA-Z0-9_]+\s*/.exec(expr)
|
||||
if (!nameMatch) throw new Error(`Missing name in "${expr}"`)
|
||||
const name = nameMatch[0].trim()
|
||||
const afterName = nameMatch[0].length
|
||||
let type: Token[] = []
|
||||
let afterType = afterName
|
||||
if (expr[afterName] == ':') {
|
||||
const typeStr = matchParen(expr.slice(afterName + 1)).slice(0, -1)
|
||||
type = tokenizeExp(typeStr)
|
||||
afterType += typeStr.length + 1
|
||||
}
|
||||
if (expr[afterType] != '.') throw new Error(`Missing dot in "${expr.slice(afterType)}"`)
|
||||
const body = tokenizeExp(expr.slice(afterType + 1))
|
||||
return ['lambda', name, type, body]
|
||||
}
|
||||
|
||||
// Problem
|
||||
// \f:\a:a.a.\a:a.a
|
||||
|
||||
function tokenizeExp(expr: string): Token[] {
|
||||
if (expr == '') return []
|
||||
const ws = /^(\s|\n)+/.exec(expr)
|
||||
if (ws) return [['whitespace', ws[0]], ...tokenizeExp(expr.slice(ws[0].length))]
|
||||
const keyword = /^(export|import|default|replacing)\s/.exec(expr)
|
||||
if (keyword) return [['keyword', keyword[0]], ...tokenizeExp(expr.slice(keyword[0].length))]
|
||||
const macro = /^:=|^=\-?([\d\_a-fA-F]+(\.[\d\_a-fA-F]+)?(p\-?[\d_]+)?)?=>/.exec(expr)
|
||||
// const macro = /^[:<]=(([\d_]+(\.[\d_]+)?)?=>?)?/.exec(expr)
|
||||
if (macro) return [['macro', macro[0]], ...tokenizeExp(expr.slice(macro[0].length))]
|
||||
const number = /^\d\S*/.exec(expr)
|
||||
if (number) return [['number', number[0]], ...tokenizeExp(expr.slice(number[0].length))]
|
||||
const name = /^[A-Za-z0-9_]+/.exec(expr)
|
||||
if (name) return [['name', name[0]], ...tokenizeExp(expr.slice(name[0].length))]
|
||||
if (expr.startsWith("--[")) {
|
||||
let end = expr.indexOf("]--") + "]--".length;
|
||||
return [
|
||||
['comment', expr.slice(0, end)],
|
||||
...tokenizeExp(expr.slice(end))
|
||||
]
|
||||
}
|
||||
if (expr.startsWith("--")) {
|
||||
let end = expr.indexOf("\n");
|
||||
return [
|
||||
["comment", expr.slice(0, end)],
|
||||
...tokenizeExp(expr.slice(end))
|
||||
]
|
||||
}
|
||||
if (expr.startsWith('\\')) {
|
||||
const lambda = matchParen(expr)
|
||||
return [parseLambda(lambda), ...tokenizeExp(expr.slice(lambda.length))]
|
||||
}
|
||||
if (expr.startsWith('"')) {
|
||||
let i = '"'.length;
|
||||
for (; i <= expr.length; i++) {
|
||||
if (expr[i] == '\\') i++;
|
||||
if (expr[i] == '"') break;
|
||||
}
|
||||
return [
|
||||
["string", expr.slice(0, i+1)],
|
||||
...tokenizeExp(expr.slice(i+1))
|
||||
]
|
||||
}
|
||||
const ph = /^\$[a-zA-Z0-9_]+/.exec(expr)
|
||||
if (ph) return [['placeholder', ph[0]], ...tokenizeExp(expr.slice(ph[0].length))]
|
||||
const opChars = /^[^\sa-zA-Z0-9_\$\\]+/.exec(expr)
|
||||
if (opChars) return [['operator', opChars[0]], ...tokenizeExp(expr.slice(opChars[0].length))]
|
||||
throw new Error(`Logic error: none of the regices in a complete cover matched "${expr}"`)
|
||||
}
|
||||
|
||||
function nameStyle(level: number | undefined): JSX.CSSProperties {
|
||||
return {
|
||||
color: level === undefined
|
||||
? "hsl(30, 50%, 70%)"
|
||||
: `hsl(
|
||||
calc(170 - ${level} * 5),
|
||||
calc(50% + ${level} * 10%),
|
||||
calc(70% - ${level} * 5%)
|
||||
)`,
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
text?: string,
|
||||
tokens?: Token[],
|
||||
vlvlv?: Map<string, number>
|
||||
}
|
||||
|
||||
let { text, tokens, vlvlv = new Map() }: Props = Astro.props;
|
||||
|
||||
const nextLvl = vlvlv.size + 1
|
||||
|
||||
const outTokens = tokens ? tokens : tokenizeExp(text!.trim());
|
||||
---
|
||||
|
||||
<code style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
padding: "unset",
|
||||
background: "unset",
|
||||
borderRadius: "unset",
|
||||
border: "unset",
|
||||
fontFamily: '"Droid Sans Mono", monospace',
|
||||
fontSize: "small",
|
||||
}}>
|
||||
{outTokens.map(([name, value, ...extras], i) => { switch (name) {
|
||||
case 'comment': return <span style={{ color: "#8f8" }}>{value}</span>
|
||||
case 'name': return <span style={nameStyle(vlvlv.get(value))}>{value}</span>
|
||||
case 'operator': return <span style={{ color: "white" }}>{value}</span>
|
||||
case 'whitespace': return <span>{value}</span>
|
||||
case 'placeholder': return <span style={{ color: "#bb5" }}>{value}</span>
|
||||
case 'macro': return <span style={{ color: "#f55" }}>{value}</span>
|
||||
case 'keyword': return <span style={{ color: "#39f" }}>{value}</span>
|
||||
case 'string': return <span style={{ color: "#f8b" }}>{value}</span>
|
||||
case 'number': return <span style={{ color: "#afa" }}>{value}</span>
|
||||
case 'lambda':
|
||||
const sub_vlvlv = new Map(vlvlv)
|
||||
sub_vlvlv.set(value, nextLvl)
|
||||
return <span data-name={value}>
|
||||
<span style={{ color: "#999" }}>\</span>
|
||||
<span style={nameStyle(vlvlv.get(value))}>{value}</span>
|
||||
{extras[0]!.length? <>
|
||||
<span style={{ color: "#999" }}>:</span>
|
||||
<span>
|
||||
<Astro.self vlvlv={sub_vlvlv} tokens={extras[0]!} />
|
||||
</span>
|
||||
</> :null}
|
||||
<span style={{ color: "#999" }}>.</span>
|
||||
<span>
|
||||
<Astro.self vlvlv={sub_vlvlv} tokens={extras[1]!} />
|
||||
</span>
|
||||
</span>
|
||||
}})}
|
||||
</code>
|
||||
20
src/components/NavLink.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
|
||||
interface Props extends HTMLAttributes<"a"> {
|
||||
href: string,
|
||||
zone?: string,
|
||||
}
|
||||
|
||||
const { href, zone, ...attrs } = Astro.props;
|
||||
|
||||
const isActive = Astro.url.pathname == href
|
||||
|| Astro.url.pathname.startsWith((zone ? zone : href) + '/');
|
||||
---
|
||||
|
||||
<a href={href} class:list={[
|
||||
"hover:bg-shade py-2 px-1 md:px-4 flex-auto md:flex-initial",
|
||||
isActive && "font-bold text-white"
|
||||
]} {...attrs}>
|
||||
<slot/>
|
||||
</a>
|
||||
@@ -1,5 +1,5 @@
|
||||
// Place any global data in this file.
|
||||
// You can import this data from anywhere in your site by using the `import` keyword.
|
||||
|
||||
export const SITE_TITLE = 'Astro Blog';
|
||||
export const SITE_DESCRIPTION = 'Welcome to my website!';
|
||||
export const SITE_TITLE = `Lawrence's blog`;
|
||||
export const SITE_DESCRIPTION = `semiannual articles about programming, Rust and langdev`;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Temporal } from '@js-temporal/polyfill';
|
||||
import { glob } from 'astro/loaders';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { isValidTime } from './utils/time';
|
||||
|
||||
const blog = defineCollection({
|
||||
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
||||
@@ -7,11 +9,14 @@ const blog = defineCollection({
|
||||
// Type-check frontmatter using a schema
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
// Transform string to Date object
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
heroImage: z.string().optional(),
|
||||
summary: z.string(),
|
||||
image: z.string().optional(),
|
||||
// Transform string to ZonedDateTime object
|
||||
pubDate: z.string().refine(isValidTime),
|
||||
updatedDate: z.string().refine(isValidTime).optional(),
|
||||
unlisted: z.boolean().optional(),
|
||||
author: z.string(),
|
||||
tags: z.array(z.string())
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
36
src/content/blog/2022-02-26T20_21_hello_world.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Hello World!
|
||||
author: lbfalvy
|
||||
tags: [programming, web development, blog]
|
||||
pubDate: 2022-02-26T20:21Z[UTC]
|
||||
image: https://www.fox46.com/wp-content/uploads/sites/109/2018/04/92a7b31a-FD_409A20Space20Shuttle20Anniversary20RECT0_1523400530953.jpg_5314903_ver1.0.jpg?w=1280&h=720&crop=1
|
||||
summary: How I wrote a backend-free blog using React
|
||||
unlisted: false
|
||||
---
|
||||
import LambdaHighlight from '../../components/LambdaHighlight.astro'
|
||||
|
||||

|
||||
|
||||
When one thinks about starting a blog, the first go-to solution is probably WordPress. Unfortunately (or fortunately) I'm a web developer, so the inefficiency, inconvenience and general quality of WordPress bother me a lot, not to mention that it's a server-side system and my deployment target is Github Pages, which only accepts static files.
|
||||
|
||||
### Why?
|
||||
|
||||
You may think that this entire endeavor is stupid and I wasted my time. And you would have a point, it was certainly a huge effort, and there are certainly existing solutions for static-site blogging, probably even better than I can afford to make this. But here's the thing: *I really like React.* To be precise, I really like how the entire ecosystem is composed of small and flexible programs that do one thing and do it well. This is the same principle, popularised by UNIX, that enables Bash together with the GNU coreutils to do everything from [collecting the news](https://gist.github.com/lbfalvy/676e7f31dc55b8e2ff71d4b9c639dfd3) through various minor automation tasks all the way to container orchestration as seen in Docker Compose.
|
||||
|
||||
The entire project is built in this spirit. I use Typescript and Scss for most of the work and Vite bundles everything. Articles are written in MDX, which is a variant of Markdown with React component support and itself connects Remark and Rehype to turn markdown into HTML. The result is unparallelled flexibility, since I can add plugins separately to my bundler and markdown parser and nothing has to know about anything else.
|
||||
|
||||
Details like server-side rendering for search-engine support and RSS feed generation are done using custom-made scripts. SSR uses simple-ssr which is basically a wrapper on Puppeteer which is a scripting interface for Chromium. RSS dynamically loads metadata from the articles folder. On the other hand, I found that React's Suspense is a deviation from the ideas of flexibility and modularity because it forces both fetch library authors and child components to acknowledge its existence. Because of this, I'm actually using [a custom solution](https://github.com/lbfalvy/react-await) for lazy loading which fulfills these principles much better, at a small cost of efficiency.
|
||||
|
||||
And the result is actually a really good blog engine. I have React Router's lightning fast page loads, search engine indexing and scaffolding with my own Node and Bash scripts. Styling is all done via Sass which is bliss to use and I have a dev server with hot reload which feels almost as streamlined as a WYSIWYG editor except it doesn't break version control. What's not to love?
|
||||
|
||||
### Principle
|
||||
|
||||
The key concept is actually very simple: lazy loading doesn't require any runtime information about the loaded resource besides the URL, so the total size of a website segmented into lazy loaded pages is unbounded. In the current form of the blog every article is represented by a component which is lazy loaded and some metadata which is currently eagerly loaded with the article list view, but as the content grows that may change. If need arises, I can write scripts to generate
|
||||
|
||||
- separate chunks for pagination
|
||||
- filtered versions of the index for tag-based search
|
||||
- word-frequency indexes to imitate full text search
|
||||
|
||||
### Usage
|
||||
|
||||
If this sounds like something you would want on your blog, go check out [the repository](https://github.com/lbfalvy/lbfalvy.github.io). Do take note though, this is not a public package and it requires a bit of tinkering to get it to work. Maybe I'll release a framework later, but it would take a bit of rewiring and further abstraction.
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Lambda Calculus for Practical Use
|
||||
author: lbfalvy
|
||||
tags: [programming, langdev, orchid]
|
||||
pubDate: 2022-03-07T21:25Z[UTC]
|
||||
image: https://i.ibb.co/nLd7Q7H/Screenshot-2022-03-08-19-17-48.png
|
||||
summary: Designing a functional programming language
|
||||
unlisted: false
|
||||
---
|
||||
import LambdaHighlight from '../../components/LambdaHighlight.astro'
|
||||
|
||||
## Table of contents
|
||||
|
||||
##
|
||||
|
||||
Lambda calculus is a mathematical approach to abstract machines that served as the basis of languages such as Haskell and ML. As a programmer who had mainly worked with procedural languages, it always intrigued me how such languages are constructed. Recently I made an educational (Lambda calculus executor)[https://www.lbfalvy.com/f.engine] combined with an interactive tutorial to show the language to a friend, and this gave me a bit of insight into the general ideas of the language. In this series I hope to successfully define a language which is both usable and pure by applying the code quality principles I learned in Typescript onto Lambda Calculus.
|
||||
|
||||
If you aren't familiar with Lambda calculus, I suggest reading through the above tutorial or the [Wikipedia page](https://en.wikipedida.org/wiki/Lambda_calculus).
|
||||
|
||||
## Rules
|
||||
|
||||
### Substitution
|
||||
|
||||
The basic rule of the language is that of lambda calculus, that is, substitution or function application. Evaluation at runtime is always lazy, and this is sufficient for Turing-completeness. Of course, this one rule is not nearly enough for a usable language, so let's set out some more.
|
||||
|
||||
### Macros
|
||||
|
||||
Preprocessor macros in C are a language of their own, frequently used for
|
||||
|
||||
- defining invariants (except you can't be sure that it's an invariant and not a runtime expression)
|
||||
- changing the code depending on compiler flags (necessary for platform-specific behavior)
|
||||
- constructing complex parameterised types (C's type system is turing complete if we consider the preprocessor part of it, and we should because it's the official answer to generics)
|
||||
|
||||
Now, these are very different challenges and as far as I know no language apart from C opted to solve all of them by embedding another Turing-complete language.
|
||||
|
||||
Invariants are an important feature and we definitely need to support them. Since Lambda expressions are both operators and values, these invariants will be both named functions and constants.
|
||||
|
||||
Platform-specific behavior is an antipattern. A lot of C programmers will fight me over this, but in my opinion if two things share functionality but do slightly different things then they should be separated into a function that always does the same thing and two entry points which call it differently.
|
||||
|
||||
One thing C's preprocessor doesn't do is define operators, but this is something I'd like to solve in our case with macros. Infix operators are immensely useful in mathematical code but they're one of those things lambda calculus doesn't have simply because it was designed to be a mathematical lab rat and not a programming language. Do take note though, that supporting user-defined operators implies that we must also support overloading - resolving functions based on the types of their arguments.
|
||||
|
||||
### Types
|
||||
|
||||
The best thing about functional languages is the type system. Lambda calculus doesn't have types and it doesn't need them, since the only type is the lambda function that is applied to a lambda function and evaluates to a lambda function. The named constants you see in examples are actually an extension, usually representing the type of predicates from simply typed lambda calculus (a "thing" that cannot be applied to anything).
|
||||
|
||||
In many functional languages the type system is regarded as something isolated and intangible, and entire language features are scrapped to keep type inference computable. Some lack syntax for generics altogether. In my opinion code is written for humans to read and if they can't figure it out then the lack of type annotations is a flaw. Consequently, I'd like my type system to be as expressive as technically possible and intentionally ignore such questions as the computability of type inference.
|
||||
|
||||
On the other hand, I don't want to fall into C's trap and make the type system a separate langauge, as that would increase the maintenance effort and steepen the learning curve disproportionately. What I would ike is for the type system to operate on the same principles as the language itself, with the same or similar syntax elements.
|
||||
|
||||
As for syntax, I'm faced with an interesting challenge; choosing to ignore type inference means that users may have to add type hints to intermediary values, but functional languages doon't normally include variable declarations - or variables for that matter - so type hints will have to be designed in such a way that any subsection of the program that has a type can receive one.
|
||||
|
||||
## Syntax
|
||||
|
||||
### Basics
|
||||
|
||||
While experimenting with f.engine, I found that the following syntax works really well with common Lambda calculus:
|
||||
|
||||
<LambdaHighlight text="
|
||||
func=\arg:type. operation arg
|
||||
func value:type
|
||||
-- this is a comment
|
||||
"/>
|
||||
|
||||
With a little bit of syntax highlighting this can be very clean and concise so I'll stick to it. One thing to note here is that while Arg is required to be a name, Type can be any valid lambda expression. A type preceded by a colon can be attached to any expression, but in many cases (like the provided example) the language should be able to infer the types if all expression parameters are typed correctly.
|
||||
|
||||
### Macros
|
||||
|
||||
Macros are parsed and executed after tokenization but before parsing and specify substitution rules in terms of a list of tokens. The basic syntax defines a source and a target which share a number of placeholders, a floating point priority which defaults to 1 and a direction of parsing.
|
||||
|
||||
<LambdaHighlight text="
|
||||
$a + $b =5=> (add $a $b)
|
||||
$a * $b =6=> (mult $a $b)
|
||||
if $a then $b else $c <=2= ($a $b $c) -- assuming Church booleans
|
||||
-- These three map square bracketed lists to a custom function
|
||||
-- and church booleans
|
||||
[ := (begin_list
|
||||
, := false
|
||||
] := true)
|
||||
"/>
|
||||
|
||||
A $placeholder can represent either a single token or a group of tokens between balanced parentheses. Parentheses aren't considered tokens for the purposes of macro resolution, but they may appear in the target. Tokenization is done via a standard greedy strategy, with the exact set of tokens defined by the macros.
|
||||
|
||||
This should be enough for most things we might want to do, but it's far too verbose for defining functions and invariants so we need a shorter syntax. If the source is a single token literal followed by a single equals sign, the target is automatically parenthesized:
|
||||
|
||||
<LambdaHighlight text="
|
||||
true := \true.\false.true
|
||||
"/>
|
||||
|
||||
### Conclusion
|
||||
|
||||
Given the above examples and fairly standard Lambda Calculus constants, the below example is a reasonable guess at what the final code might look like:
|
||||
|
||||
<LambdaHighlight text="
|
||||
find := \T. \predicate:(\item:T.Bool). \list:Cons T. (
|
||||
(loop \repeat. \sublist:Cons T.
|
||||
if (predicate (head sublist))
|
||||
then (head sublist)
|
||||
else (repeat (tail sublist))
|
||||
) list
|
||||
)
|
||||
"/>
|
||||
|
||||
Here `loop` is a more accessible keyword for the Y combinator and `T` is a generic parameter. Also note that the type of sublist is specified even though it can easily be deduced from context because the human reader parsing the code top-down would have to skip to the end of the loop call to see the parameter. This is a good example of a case where adding superfluous type hints improves readability.
|
||||
|
||||
## Future
|
||||
|
||||
This is the first article in a series discussing aspects of programming language design. In the following parts I'll discuss type checking and overloading, type inference, guaranteed optimizations and elements of the standard library, in no particular order. I hope to write an implementation of the language during the next academic year, by then it would be best to settle on most high level design decisions.
|
||||
|
||||
_ps: I updated the code samples in this article to keep the highlighting working. Changes:_
|
||||
|
||||
- _replaced `=` and `==` with `:=` in invariant rules_
|
||||
148
src/content/blog/2022-08-19T17_19_orchid.mdx
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: Macros in Orchid
|
||||
author: lbfalvy
|
||||
tags: [programming, langdev, orchid]
|
||||
pubDate: 2022-12-04T17:19Z[UTC]
|
||||
summary: A preprocessor based on generalized kerning
|
||||
unlisted: false
|
||||
---
|
||||
import LambdaHighlight from '../../components/LambdaHighlight.astro'
|
||||
|
||||
### The story so far
|
||||
|
||||
Almost nine months ago [I wrote an article](/blog/2022-02-27T21_25_lambda_calculus_for_practical_use) about a programming language I was working on. In it I avoided naming the language, spoke very vaguely about the motivations behind the project and made several logical errors in my code samples. Since then I've been working a lot on [the language now called Orchid](https://github.com/lbfalvy/orchid). My final year project will be the type system, and I will complete the other parts later.
|
||||
|
||||
Orchid is essentially Lambda calculus, with the addition of type annotations. Polymorphism is parametric and typeclasses are somewhat similar to Rust's traits, although they support HKTs and handle incoherence with flexible overrides rather than the rigid orphan rules. I don't want to go into much more detail here because the type system is still under planning, these are essentially the design parameters.
|
||||
|
||||
The language works with file-based namespaces not unlike Rust.
|
||||
|
||||
The key feature that makes Orchid useful and sets it apart somewhat is the macro system. Macros consist of substitution rules with placeholders and a real numbered priority. Macros have no clear range of effect, but they operate on namespaced tokens which means that invoking a module is always intentional but rules from that module file are then free to propagate the effects of that invocation all around the expression tree. This network of effect-vines growing all over the tree and transforming it is where the language gets its name from. The macro engine is also the only part of the language that is fully completed at the time of writing and is the topic of this article.
|
||||
|
||||
This macro language is based on generalized kerning, a well-known Turing-complete problem. An excellent breakdown of the proof can be seen in [this video](https://youtu.be/8_npHZbe3qM). The part of the proof that caught my attention was how "carriages" can be used for scanning or traversing the token sequence. A carriage can be intuitively recognized as a distinguishing token and possibly some payload for which a special traversal rule(set) exists that moves it recursively in some direction. Below is a typical carriage:
|
||||
|
||||
<LambdaHighlight text="
|
||||
start_collection =9=> collection_carriage(())
|
||||
collection_carriage($payload) $data =10=> $data collection_carriage($payload)
|
||||
collection_carriage($payload) add $item =11=> collection_carriage(($item $payload))
|
||||
collection_carriage($payload) end_collection =11=> $payload
|
||||
"/>
|
||||
|
||||
This carriage goes from any start_collection token until the next end_collection, collecting all tokens prefixed by an add command and ignoring the rest. The start and end tokens are consumed with the end token being replaced by the carriage's payload:
|
||||
|
||||
<LambdaHighlight text="
|
||||
a b start_collection c d add e add f g add add h end_collection i --r1
|
||||
a b collection_carriage(()) c d add e add f g add add h end_collection i --r2
|
||||
a b c collection_carriage(()) d add e add f g add add h end_collection i --r2
|
||||
a b c d collection_carriage(()) add e add f g add add h end_collection i --r3
|
||||
a b c d collection_carriage((e ())) add f g add add h end_collection i --r3
|
||||
a b c d collection_carriage((f (e ()))) g add add h end_collection i --r2
|
||||
a b c d g collection_carriage((f (e ()))) add add h end_collection i --r3
|
||||
a b c d g collection_carriage((add (f (e ())))) h end_collection i --r2
|
||||
a b c d g h collection_carriage((add (f (e ())))) end_collection i --r4
|
||||
a b c d g h (add (f (e ()))) i --no match
|
||||
"/>
|
||||
|
||||
Of course, this carriage isn't particularly useful, and defining stuff like infix operators in this fashion would be needlessly painful, so Orchid also comes with vectorial (as opposed to scalar) placeholders. Two variants are available:
|
||||
|
||||
<LambdaHighlight text="
|
||||
...$data:2 -- matches 1..n tokens
|
||||
..$data:2 -- matches 0..n tokens
|
||||
"/>
|
||||
|
||||
The 2 in these placeholders is their _growth priority_. This has nothing to do with the rules' priority, and it's an integer value. In essence, if there are multiple ways for a pattern to match a given sequence, the one where the vectorial placeholder with the greatest priority matches the longest subsequence will be replaced first. In case of a tie the second placeholder in priority is considered and so forth. These growth priorities are only relevant within a token sequence, vectorials inside braces are always prioritized lower than vectorials outside them. The default priority is the minimum, 0.
|
||||
|
||||
With this knowledge, infix operators may be defined like so:
|
||||
|
||||
<LambdaHighlight text="
|
||||
...$lhs:1 + ...$rhs =50=> add (...$lhs) (...$rhs)
|
||||
...$lhs:1 * ...$rhs =45=> mul (...$lhs) (...$rhs)
|
||||
...$arg -> ...$ret:1 =55=> fn (...$arg) (...$ret)
|
||||
-- Haskell's function-notation is right-associative
|
||||
"/>
|
||||
|
||||
Notice that multiplication has lower priority than addition, because both operators are eager. We could've also defined them both using scalar placeholders and their normal priority order, but that way to add something to the result of a function call we would have had to parenthesize the call. In practice, we would like for function calls to have the highest perceived priority, so operators are located in reverse order and assumed to apply to the entire preceding and following section of the expression.
|
||||
|
||||
To demonstrate the utility of this system, here's a set of rules to transform a classic array expression in square brackets into a conslist:
|
||||
|
||||
<LambdaHighlight text="
|
||||
-------- In main --------
|
||||
main := [foo, bar, baz, quz]
|
||||
|
||||
-------- In prelude --------
|
||||
[...$item , ...$rest:1] =2=> (some (pair (...$item) [...$rest]))
|
||||
[...$only] =1=> (some (pair (...$only) []))
|
||||
[] =1=> none
|
||||
"/>
|
||||
|
||||
I was going to include the ruleset for a match expression in this article too, but I realized that the specifics of how control may flow and types may be annotated across match arms depends heavily on the type system because ultimately the handlers need to be converted to N-ary lambda functions (N being the number of distinct placeholders) and the type checker needs to be able to statically assert from the match expression that N arguments will in fact be provided.
|
||||
|
||||
I also wrote a small demo using the example of monads to demonstrate how lambda expressions are embedded in templates. For the purposes of this demo, `@T.` indicates a generic over `T`, and `@:T U V.` indicates a generic constraint for the existence of the relation or trait `T` for parameters `U` and `V`. For a rough explanation of how these work consult [the project readme](https://github.com/lbfalvy/orchid#auto-parameters-generics-polymorphism). Minor differences in semantics are to be expected, the type system is still in early development.
|
||||
|
||||
<LambdaHighlight text='
|
||||
-------- In main --------
|
||||
import std::io::(readln, print)
|
||||
import std::string::f
|
||||
|
||||
export main := (
|
||||
print "Username: ";
|
||||
username = readln;
|
||||
print(f"Password for {}:" username);
|
||||
-- auth code...
|
||||
)
|
||||
|
||||
-------- In prelude --------
|
||||
--[ Functions defined:
|
||||
export bind: @M. @:Monad M. @T. @U. (T -> M U) -> M T -> M U
|
||||
export wrap: @M. @:Monad M. @T. T -> M T
|
||||
export put: @M. @:Monad M. @T. @U. M U -> M T -> M U
|
||||
]--
|
||||
-- Monads that wrap a value support procedural-like assignment syntax
|
||||
$_name = ...$first ; ...$second =2002=> bind (\$_name. ...$second) (...$first)
|
||||
-- Monads of any sort can be chained
|
||||
...$first ; ...$second =2001=> put (...$second) (...$first)
|
||||
-- Plus the above conslist example
|
||||
export ::(=, ;)
|
||||
|
||||
-------- In std::string --------
|
||||
--[ Functions defined:
|
||||
export format: String -> Cons String -> String
|
||||
export to_string: @T. @:ToString T. T -> String
|
||||
]--
|
||||
import std::static::static_map
|
||||
-- Transform variadic arguments to conslist of strings
|
||||
export ..$prefix:1 f $template ..$values =501=> ..$prefix format $template static_map to_string [..$values]
|
||||
|
||||
-------- In std::static --------
|
||||
-- Recursively prefix every element of a statically known conslist with a function
|
||||
static_map $fn (some (cons $item $tail)) =801=> (some (cons ($fn $item) static_map $fn $tail))
|
||||
static_map $fn none =801=> none
|
||||
export ::(static_map)
|
||||
'/>
|
||||
|
||||
`static_map` as used by the `f` shorthand is also a fine example of a carriage, and of a helper rule being invoked by another rule without the programmer's knowledge to automate some subtask. Rules can form ecosystems like this, providing hooks or delegating work.
|
||||
|
||||
An ant property of this system is that compile-time structures can leak into runtime code. This is very useful in general because it enables a lot of powerful patterns for optimizing runtime, value-universe code depending on calling context, but in some cases it can cause problems on the boundary that are difficult to trace, for example, if `static_map` is called on a runtime conslist it will be left in the source even though it doesn't (and depending on the capabilities of the type system possibly can't) have a runtime definition. This problem can be solved with very low priority error reporting rules. The following is just an example:
|
||||
|
||||
<LambdaHighlight text='
|
||||
-------- In std::macro_error --------
|
||||
-- the first argument contains internal state, the second arguments
|
||||
-- construct as "count_tokens () (..$some_sequence)"
|
||||
count_tokens (..$increments) ($next ..$rest) =11_000=> count_tokens (..$increments + 1) (..$rest)
|
||||
count_tokens (..$incrememnts) () =11_000=> (0 ..$increments)
|
||||
-- traverse a layer up while recording position
|
||||
(..$left macro_error ($path) $payload ..$right) =10_000=> macro_error (
|
||||
(count_tokens () (..$left)) $path
|
||||
) $payload
|
||||
-- translate any type of brackets into parentheses
|
||||
[..$left macro_error ..$right] =10_000=> (..$left macro_error ..$right)
|
||||
{..$left macro_error ..$right} =10_000=> (..$left macro_error ..$right)
|
||||
|
||||
export ::(macro_error)
|
||||
|
||||
-------- Addition to std::static --------
|
||||
import std::macro_error::macro_error
|
||||
|
||||
static_map =-100=> macro_error () "Argument should be a conslist"
|
||||
'/>
|
||||
|
||||
Now when static_map is accidentally left in the code after all other macros had been executed, it will be converted to an error which will then bubble up to the AST root. Debugging tools can recognize these and produce a human-readable error trace.
|
||||
55
src/content/blog/2022-12-02T17_36_orchids_type_system.mdx
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: "Orchid's type system: cross-universe recursion"
|
||||
author: lbfalvy
|
||||
tags: [programming, orchid]
|
||||
pubDate: 2022-12-02T17:36Z[UTC]
|
||||
summary: An early progress report on my plans for Orchid's type system
|
||||
unlisted: true
|
||||
---
|
||||
|
||||
## Unification, universe recursion
|
||||
|
||||
Orchid will be a compiled variant of lambda calculus which supports dependent types. Because it's compiled, the type of every value must be known statically, and because it supports dependent types, values can spontaneously appear in type expressions. This makes the type system Turing complete, and introduces the concept of dead branches.
|
||||
|
||||
As a matter of fact, Orchid's type system is Orchid. During unification, possible reductions of both type expressions are iterated breadth-first, and each of them are unified with each reduction of the opposite type until a match is found, the sequences end or a maximum number of steps is reached. In this way, by adding recognized reduction steps, we can support selected cases of infinite recursion in a manner that is backwards-compatibly extensible.
|
||||
|
||||
Unfortunately, there's another kind of recursion, one which is much more difficult to detect. Recursion across universes. Through a certain kind of symmetry between values and types, functions and kinds, it's very easy to end up with a value that occurs in a dead branch of its own type. Eagerly type checking this value would reduce to the same problem in the universe of types.
|
||||
|
||||
At this point it would make a lot of sense to just classify this as unsupported behavior. It would simplify the language substantially, and it would probably still leave a very expressive language. But saying no to myself isn't what got me so far. Instead, my solution is to employ gradual typing for type expressions. Above the universe of values, types are only unified when actually evaluating a function on a parameter.
|
||||
|
||||
## HKTs, typeclasses
|
||||
|
||||
Orchid supports higher kinded types and typeclasses. Both are defined with a `define Name $Param:kind,* as ...typeExpression` statement, which specifies all parameters and their kinds, and the backing type. For a demonstration, here's what the definition of List (type constructor) and Add (typeclass) might look like
|
||||
|
||||
```orchid
|
||||
define List $T:type as loop \r. Option (Pair $T r)
|
||||
define Add $A $B $R as $A -> $B -> $R
|
||||
define Multiply $A $B $R as $A -> $B -> $R
|
||||
define Default $T:type as $T
|
||||
```
|
||||
|
||||
This also demonstrates how recursive types work. These type constructors can be partially applied and even inferred in that form. For unification purposes, Add is an opaque function and can only unify with itself or a variable, not Multiply. The functions `destruct` and `construct` translate between defined types and definitions. To implement a typeclass, `impl ...typeExpression via ...valueExpression` statements are used. These take a couple forms, but their general components are
|
||||
|
||||
- `impl @V. @:Superclass Variable. Class Variable` _required_ Uses that unify with this type are implemented by the candidate
|
||||
- `by implementationName` _optional_ A value by this name is defined in the local scope and exported. The type of the value is the definition of the typeclass, not the typeclass itself.
|
||||
- `over otherImplementation, thirdImplementation` _optional, requires "by"_ The implementation supersedes these specified other implementations.
|
||||
- `via valueExpression` _required_ The value that will be applied wherever this typeclass successfully matches. This will receive all auto parameters and preconditions that occur in the type expression, and must have the same type as the definition of the typeclass.
|
||||
|
||||
Following are some examples of typeclass implementations.
|
||||
|
||||
```orchid
|
||||
impl Add string string string via \a.\b. strcat a b
|
||||
impl @T. -- variables
|
||||
@:Add T T T. @init:Default T. -- preconditions
|
||||
Multiply uint T T -- target
|
||||
by iterativeMultiply
|
||||
via \n.\t. (loop \r. \i.
|
||||
if i = 0 then init else t + r (i - 1)
|
||||
) n
|
||||
```
|
||||
|
||||
Typeclasses are referenced in value-code the same way preconditions are referenced above. These instances are unified with every potential impl. A unifying candidate is accepted if it directly or indirectly overrides every other unifying candidate. _Ambiguity is allowable in conceptual models, but the concrete decisions that influence compiled code always must be based on a traceable sequence of direct commands._
|
||||
|
||||
## SFINAE
|
||||
|
||||
This should hopefully produce a behavior very similar to C++'s SFINAE. You CAN pass an invalid argument to a type constructor, and you CAN then use that type in any kind of complex type expression, but this branch will never be reduced and will never unify with anything, so only expressions where that branch is eliminated can pair with it during impl resolution.
|
||||
159
src/content/blog/2023-12-18T17_36_a_clever_trait_pattern.mdx
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: Multiple First-party Blanket Implementations
|
||||
author: lbfalvy
|
||||
tags: [programming, rust]
|
||||
pubDate: 2023-12-19T17:20Z[UTC]
|
||||
summary: Breakdown of an interesting technique that gave me a new perspective on Rust traits
|
||||
unlisted: false
|
||||
---
|
||||
import CodeError from '../../components/CodeError.astro'
|
||||
|
||||
I recently saw a clever techinque [on the Rust forum](https://users.rust-lang.org/t/two-blanket-implementations-for-different-classes-of-objects/100173/3) which reshaped my understanding of traits. It took me a while to fully understand why this technique works and I found the conclusion very satisfying, so I'll break it down in this article.
|
||||
|
||||
Let's suppose that we have the following structure. This is a contrived example vaguely inspired by Minecraft, but the general situation is fairly common.
|
||||
|
||||
```rust
|
||||
trait Entity {}
|
||||
trait Block {}
|
||||
|
||||
struct Player;
|
||||
impl Entity for Player {}
|
||||
struct Enemy;
|
||||
impl Entity for Enemy {}
|
||||
|
||||
struct Stone;
|
||||
impl Block for Stone {}
|
||||
struct Ore;
|
||||
impl Block for Ore {}
|
||||
|
||||
trait HitTarget {}
|
||||
```
|
||||
|
||||
Now let's suppose that we want to implement `HitTarget` for all blocks and entities.
|
||||
|
||||
```rust
|
||||
impl<T> HitTarget for T where T: Block {}
|
||||
impl<T> HitTarget for T where T: Entity {}
|
||||
```
|
||||
|
||||
<CodeError text="
|
||||
error[E0119]: conflicting implementations of trait 'HitTarget'
|
||||
"/>
|
||||
|
||||
The above code doesn't compile, because there could be a type which implements both `Block` and `Entity`. In general, we can't make negative statements (statements about the absence of an implementation) about traits so in order to prove that a trait's implementations never overlap, only one blanket implementation is ever permitted. So how do we proceed?
|
||||
|
||||
# Sets of traits
|
||||
|
||||
A generic trait or a generic type is an infinite set of traits or types that share some properties and so you can reason about the whole set, or subsets of it, with your logic applying to elements you don't (can't) know about. I used to think about the generic itself as the trait or type, this was one of my major misconceptions. Think of `Index<T>`, which can invoke entirely different (though logically related) behaviour on the same containers as `Index<usize>` and `Index<Range<usize>>`.
|
||||
|
||||
With this in mind, an impl is actually an infinite set of associations, each between a trait and a type. We already covered blanket implementations, but consider the following impl, which actually appears in the source code of Orchid:
|
||||
|
||||
```rust
|
||||
impl<T: ?Sized> Index<T> for VPath where Vec<Tok<String>>: Index<T> { }
|
||||
```
|
||||
|
||||
This asserts, in common English, that `VPath` can be indexed by anything `Vec<Tok<String>>` can be indexed by. Because the set of possible values of `T` is open the only thing this implementation can do with its argument is index a vector, but in this case `VPath` is a thin wrapper around a vector with some convenience features so this works out just fine.
|
||||
|
||||
With this in mind, we can try to implement `HitTarget` by using a generic trait to be able to talk about both all of something and one of it:
|
||||
|
||||
```rust
|
||||
trait HitTargetHelper<T> {}
|
||||
|
||||
struct BlockHitHelper;
|
||||
struct EntityHitHelper;
|
||||
|
||||
impl<T> HitTargetHelper<BlockHitHelper> for T where T: Block {}
|
||||
impl<T> HitTargetHelper<EntityHitHelper> for T where T: Entity {}
|
||||
impl<T, U> HitTarget for T where T: HitTargetHelper<U> {}
|
||||
```
|
||||
|
||||
At a first glance, that's a lot of `*Helper` types, but this can be resolved by establishing clear vocabulary for the pattern. Either way, this doesn't compile either:
|
||||
|
||||
<CodeError text="
|
||||
error[E0207]: the type parameter `U` is not constrained by the impl trait, self type, or predicates
|
||||
" />
|
||||
|
||||
The problem with type parameters that don't appear in the trait or type is that this isn't actually a single implementation for each combination of traits and type. We want to use functionality within `HitTargetHelper<U>` to implement `HitTarget`, and when we look at the potential implementations to choose from we bump into the original problem; a type could implement both `HitTargetHelper<BlockHitHelper>` and `HitTargetHelper<EntityHitHelper>`.
|
||||
|
||||
We might try to replace `U` with `T`, but then the first two impls become identical again. We need a way to make the value of `U` uniquely dependent on `T` to fix the abovementioned error, and we need to be able to explicitly state mutually exclusive values of `U` for `Block` and `Entity` implementors.
|
||||
|
||||
# Unique types
|
||||
|
||||
Since constraints are never negative, no two trait expressions can ever be mutually exclusive. Traits are unique to the implementor and parameter values but they aren't disjoint, and types are always disjoint but the implementors and parameters of a trait aren't unique. There's another kind of relation a type can have with a trait, though; associated types are disjoint from each other and unique to the implementation, which is in turn unique to the combination of trait and implementor. Let's define a trait that _uniquely selects one of_ the implementations of `HitTarget` by way of an associated type;
|
||||
|
||||
```rust
|
||||
// General boilerplate
|
||||
trait HitTargetImpl<T> {}
|
||||
impl<T> HitTarget for T where T: HitTargetPick + HitTargetImpl<T::Key> {}
|
||||
trait HitTargetPick {
|
||||
type Key;
|
||||
}
|
||||
|
||||
// specific to Block
|
||||
struct HitTargetByBlock;
|
||||
impl<T> HitTargetImpl<HitTargetByBlock> for T
|
||||
where T: Block + HitTargetPick<Key = HitTargetByBlock>
|
||||
{}
|
||||
|
||||
// specific to Entity
|
||||
struct HitTargetByEntity;
|
||||
impl<T> HitTargetImpl<HitTargetByEntity> for T
|
||||
where T: Entity + HitTargetPick<Key = HitTargetByEntity>
|
||||
{}
|
||||
```
|
||||
|
||||
This way the two `HitTargetImpl` implementations are mutually exclusive and the `HitTarget` implementation selects one unambiguously.
|
||||
|
||||
With this in place, any `Block` or `Entity` can become a `HitTarget` without reimplementing any of the logic by just selecting the correct Key for the implementation:
|
||||
|
||||
```rust
|
||||
impl HitTargetPick for Enemy {
|
||||
type Key = HitTargetByEntity;
|
||||
}
|
||||
```
|
||||
|
||||
To make more traits "proxy" `HitTarget`, you can define new values of `Key` and implement `HitTargetImpl` for them. These implementations can be defined on their own or in terms of any combination of other traits. You could even define a different implementation that also proxies through `Entity`, with a different `Key` value. Indeed, the `Entity` and `Block` constraints in the `HitTargetImpl` implementations above aren't even really part of the main mechanism.
|
||||
|
||||
In Minecraft all blocks are breakable, or at least act like they are breakable even if you never actually finish breaking them. If you want to mandate that all implementors of a certain trait select the same implementation of another, you can add it as a supertrait, which allows you to simply reference `Block` and use `HitTarget` functionality. You can also of course add `HitTarget` as a supertrait directly but this doesn't force a particular implementation.
|
||||
|
||||
```rust
|
||||
trait Block: HitTargetPick<Key = HitTargetByBlock>;
|
||||
```
|
||||
|
||||
Don't forget though that blanket impl limitations still apply to `HitTargetPick`, so you can't just implement it once for all types that implement `Block`. Each block will have to implement `HitTargetPick` separately, you just get a handy error reminding you to do it.
|
||||
|
||||
It's easy and potentially helpful to qualify valid values of `HitTargetPick::Key`, here `HitTargetByBlock` and `HitTargetByEntity` with another marker trait, eg. `HitTargetChoice`, so that if users accidentally select the wrong struct and don't immediately rely on the `HitTarget` implementation they still get a type error.
|
||||
|
||||
# When to use and avoid
|
||||
|
||||
Whenever programmers discover a neat gadget like this one, we have a tendency to apply it over-eagerly. Let's consider the strengths, weaknesses, and alternatives of this pattern.
|
||||
|
||||
As mentioned above, the set of implementations is extensible by just defining `HitTargetImpl` for new values of `Key`. This of course refers to an internal extension, as blanket implementations of external traits aren't allowed. Importantly, this means that when used in an external interface, this trait creates a divide between first party implementations and third party implementations which need to use a newtype. The newtype pattern iw discussed below as an alternative.
|
||||
|
||||
Modifications and extensions to the trait itself don't affect the indirect implementors, so it's much more flexible with respect to the trait itself than, say, exposing the indirect implementations as functions and then having the individual blocks and entities implement `HitTarget` using those functions.
|
||||
|
||||
The most notable problem is that this selector mechanism is extremely heavy, and it isn't _really_ a blanket implementation, just a sort of shared implementation body. The individual types still need to request the implementation, even if they can be statically forced to do so. The mental overhead of such a system can be daunting in Rust code which already tends to be very concept-heavy.
|
||||
|
||||
## Newtype
|
||||
|
||||
The most plausible alternative I could find was to define a generic newtype:
|
||||
|
||||
```rust
|
||||
struct EntityHitTarget<T: Entity>(pub T);
|
||||
impl<T> HitTarget for EntityHitTarget<T> where T: Entity {}
|
||||
```
|
||||
|
||||
In contrast to the above discussed pattern, this one is absolutely tiny and extremely straightforward. The main drawback is that the newtype which is synonymous to the choice of `HitTarget` implementation is now stated every time a type is cast into `HitTarget`. Additionally, this can't be combined with other trait bounds at all.
|
||||
|
||||
Where this pattern really shines therefore is if you want to implement a large trait for a large number of first party disjoint groups which together contain many objects, just like the interaction model of a game.
|
||||
|
||||
# Terminology
|
||||
|
||||
Earlier I mentioned the need to establish vocabulary, and I tried to establish one with the final example. This isn't by any means authoritative, it's what I use for internal consistency in Orchid. To reiterate:
|
||||
|
||||
- The trait which is generic over the choice is simply suffixed `*Impl`. This also pairs well with another much more general trait pattern I call impl/dyn separation which relates to trait objects, I'll venture to describe it once I understand its consequences better.
|
||||
- The trait with a single associated type is suffixed `*Pick` and the associated type is `Key`. These are chosen to be as short as possible because the whole trait often appears on one line.
|
||||
- The optional marker trait for valid values of `Key` is suffixed `Choice`
|
||||
- The pattern itself is called Multiple First-party Blanket Implementations, or MFBI. It sounds nowhere near as nice as CRTP, but unlike CRTP it actually describes what the pattern does succintly and accurately.
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Designing an IPC-based plugin system for Orchid
|
||||
author: lbfalvy
|
||||
tags: [programming, orchid]
|
||||
pubDate: 2024-08-08T17:18Z[UTC]
|
||||
summary: My plans for modularizing a programming language
|
||||
unlisted: false
|
||||
---
|
||||
## The story so far
|
||||
|
||||
Orchid is an experimental programming language I've been building for about 2 years. During that time I learned a lot about Rust, language development, project management and my own pace of work. I wrote three articles on this blog detailing various aspects of the project. All of them are now dated and some are laughably ambitious (my plans for a type system for example will take another 2 years to even begin to materialize) but I'll keep them up for posterity.
|
||||
|
||||
Right now, Orchid is an interpreted, lazy, functional programming language that supports language extensions written in Rust. It supports plug-ins that are defined in Rust and can extend the lexer, parser, and define additional types and operations. Expression-level syntax is also extensible via macros which require no native code. Plugins are standalone executables which communicate with the interpreter through message passing IPC.
|
||||
|
||||
## Architecture
|
||||
|
||||
Every part of my ecosystem is written in Rust, so I elected to define the protocol in Rust too. With the help of a derive macro I can define my messages as structs which also act as the deserialized message objects in messaging code. My hope is that this definition is parseable by other languages, but this isn't an immediate priority.
|
||||
|
||||
The messaging protocol defines requests and notifications. For now it uses standard IO, but it's very likely that this will not be fast enough on the long run, so most of the ecosystem is protocol agnostic. One dependency I could not figure out how to avoid was threading; unfortunately this means that porting Orchid to Webassembly would be a nightmare, but I hope that either wasm threading support or Rust's wasm support improves by the time I get there.
|
||||
|
||||
The interpreter's most important extension mechanism is an Atom. Atoms are opaque values that carry a buffer of data, an optional ID, and the plugin that created them. When an atom is applied as a function, returned as the value of the program, or destroyed, the plugin is contacted via IPC to decide what should happen next. Plugins can also use the atoms as anchors to send messages to each other. The result is that at runtime the interpreter takes the role of an "object broker" that connects handles to objects in external programs according to the program.
|
||||
|
||||
## Lexer and parser plug-ins
|
||||
|
||||
In an effort to make Orchid as customizable as the plugin authors need, there are hooks defined in the protocol which allow the plug-ins to subscribe to two specific events.
|
||||
|
||||
1. A specific character is encountered in source text
|
||||
2. A specific word is encountered at the start of a line
|
||||
|
||||
Lexer plugins, like the lexer itself, convert source code into syntax primitives; names, parenthesized subsections, lambda functions and atoms. The fact that lambda functions are already present at this point is a choice of convenience with regard to parser design, in practice, lexers mostly produce atoms and simple expressions involving them. These also have the opportunity to recursively call back to the lexer via IPC, which enables such things as JS-style perfectly recursive string interpolation.
|
||||
|
||||
Parser plugins on the other hand define new categories of static items; their output is a subtree of modules, macro rules, and constants. This tree is static knowledge so reflection on it is a pure value-based operation, so the parser extensions define similarly static things; the standard library uses these for types and elixir-style protocols.
|
||||
|
||||
## Macros
|
||||
|
||||
Between character-level lexers and item-level static parsers, the expression level is addressed with a dramatically different mechanism; hygienic S-expression substitution rules. These are blissfully simple for easy tasks like defining an infix operator or a pipeline operator, and they can tackle many computational tasks such as list transforms with ease and style.
|
||||
|
||||
The standard library uses them to define constructor syntax for containers, procedural-style statement lists, and to establish an extensible pattern matching protocol that usercode can hook into to define its own custom patterns. The macro system allows Orchid to retain the metaprogramming power of Lisp while approaching the syntactic minimalism of Haskell.
|
||||
|
||||
## Tooling
|
||||
|
||||
I'm an avid text editor user, and unobtrusive dev tooling is close to my heart, so Orchid already has a language server, which calls the interpreter for analysis to ensure that all future plugins will be implicitly supported. My intention is to develop a test runner and formatter early on as well, as these also greatly contribute to developer experience. These are pretty mission-agnostic tools that are plain useful regardless of architecture.
|
||||
|
||||
Before all that however, it is essential that I develop some kind of package manager.
|
||||
44
src/content/blog/2025-01-27T11_47_async_iterator_map.mdx
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Async Iterator::map
|
||||
author: lbfalvy
|
||||
tags: [programming, rust, langdev]
|
||||
pubDate: 2025-01-27T10:47Z[UTC]
|
||||
summary: On the state of async Rust, limitations of the type system, and Iterator::map
|
||||
unlisted: false
|
||||
---
|
||||
The async equivalent to iterators are streams, which are exactly the same as AsyncIterators in JS or C#; the consumer pulls on the stream but the stream is allowed to defer responding, so both sides must be able to pause. Simple stuff.
|
||||
|
||||
In synchronous rust, `Iterator::map` takes an `FnMut`, a function which can only be called if you can prove that it's not already running. This is good because it's pretty common to want to either use mutable context for a transformation or equivalently perform a sequence of effectful operations and then collect their results into a datastructure, and both of these are obviously expressed as `sequence.map(|item| /* some mutation */).collect()` which as a bonus propagates size hints. The Orchid codebase is FULL of this pattern.
|
||||
|
||||
The async equivalent however has to take a function that returns some type that implements `Future` because that's how you statically type an asynchronous function, you parameterize on the state machine the compiler will eventually generate for its paused data. This is again perfectly normal, C++ coroutines do the same as I'm pretty sure every other language that supports any kind of stack-allocated coroutine has to. The problem emerges from lifetimes, because in order for that Future to hold onto a mutable reference, the async equivalent of map (which happens to be called `StreamExt::then` for reference) has to not only guarantee that the callback will not be running when its next called, but that its return value (the `Future` instance) will not exist (either because it's finished or because it's been freed) by the time the function is called again.
|
||||
|
||||
The type of the callback then has to be _some function which for any lifetime `'a` returns some type is valid for the same lifetime `'a`_. The type of the return value is parametric!
|
||||
|
||||
Actually this isn't completely impossible to represent in Rust because it has a weird bastard type system where generic parameters to traits (types that pick an implementation such as `u32` in `impl From<u32> for u64 {}`) must be concrete types, but associated types (types that are picked by the implementation such as the `str` in `impl Deref for String { type Target = str }`) may themselves be generic. So it's imaginable for callbacks to be mapped into a bound for a trait with an associated return type that's generic on a lifetime, like this:
|
||||
|
||||
```rust
|
||||
trait FnMutMap {
|
||||
type Args<'a>;
|
||||
type Output<'a>;
|
||||
fn call<'a>(&'a mut self, args: Self::Args<'a>) -> Self::Output<'a>;
|
||||
}
|
||||
```
|
||||
|
||||
Since structs and functions can only be parametric on concrete types, a callback whose return type has a different contract depending on how you called the function is illegal, so if you want to access mutable data in an async stream, you have to make an ad-hoc `Mutex<&mut T>` right there on the stack which the closure and its return value can capture by shared reference and then immediately lock for its entire runtime. Streams are lazy and a new value will not be pulled until the current one is finished so this mutex can never ever be contested, but there is no way at all to explain this to the type system.
|
||||
|
||||
There's also another trick, `for<'a> Fn(&'a [u8]) -> &'a u8` is a valid type, but this `for<'a>` syntax is again a late addition to the borrow checker which isn't integrated in the type system at all, so `for<'a> Fn(T) -> Box<dyn Future<Output = U> + 'a>` is valid but only because the higher-kinded lifetime parametrism has been localized to a single type constraint. Streams don't do this because it requires a heap allocation and subsequent virtual function calls to interact with the Future, and an uncontested mutex lock-release cycle is vastly faster.
|
||||
|
||||
So what are people doing? Well, for the most part they're using `async_stream::stream!` with a `for` loop from what I gather in a fashion a lot like the below example. This uses an MPSC to force the stream to be lazy.
|
||||
|
||||
```rust
|
||||
stream! {
|
||||
for item in item_seq {
|
||||
yield item.process(&mut mutableCtx).await
|
||||
}
|
||||
}
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
```
|
||||
|
||||
In principle this should cost a constant number of virtual calls per step when the MPSC wakes either thread, which might be faster, but I haven't measured as this is already fast enough and I'm a staunch believer in benchmarking only what needs to be faster. It's nice to keep track though.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: 'First post'
|
||||
description: 'Lorem ipsum dolor sit amet'
|
||||
pubDate: 'Jul 08 2022'
|
||||
heroImage: '/blog-placeholder-3.jpg'
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
|
||||
|
||||
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
|
||||
|
||||
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
|
||||
|
||||
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
|
||||
|
||||
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
|
||||
@@ -1,214 +0,0 @@
|
||||
---
|
||||
title: 'Markdown Style Guide'
|
||||
description: 'Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.'
|
||||
pubDate: 'Jun 19 2024'
|
||||
heroImage: '/blog-placeholder-1.jpg'
|
||||
---
|
||||
|
||||
Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.
|
||||
|
||||
## Headings
|
||||
|
||||
The following HTML `<h1>`—`<h6>` elements represent six levels of section headings. `<h1>` is the highest section level while `<h6>` is the lowest.
|
||||
|
||||
# H1
|
||||
|
||||
## H2
|
||||
|
||||
### H3
|
||||
|
||||
#### H4
|
||||
|
||||
##### H5
|
||||
|
||||
###### H6
|
||||
|
||||
## Paragraph
|
||||
|
||||
Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.
|
||||
|
||||
Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.
|
||||
|
||||
## Images
|
||||
|
||||
### Syntax
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||

|
||||
|
||||
## Blockquotes
|
||||
|
||||
The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations.
|
||||
|
||||
### Blockquote without attribution
|
||||
|
||||
#### Syntax
|
||||
|
||||
```markdown
|
||||
> Tiam, ad mint andaepu dandae nostion secatur sequo quae.
|
||||
> **Note** that you can use _Markdown syntax_ within a blockquote.
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
> Tiam, ad mint andaepu dandae nostion secatur sequo quae.
|
||||
> **Note** that you can use _Markdown syntax_ within a blockquote.
|
||||
|
||||
### Blockquote with attribution
|
||||
|
||||
#### Syntax
|
||||
|
||||
```markdown
|
||||
> Don't communicate by sharing memory, share memory by communicating.<br>
|
||||
> — <cite>Rob Pike[^1]</cite>
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
> Don't communicate by sharing memory, share memory by communicating.<br>
|
||||
> — <cite>Rob Pike[^1]</cite>
|
||||
|
||||
[^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015.
|
||||
|
||||
## Tables
|
||||
|
||||
### Syntax
|
||||
|
||||
```markdown
|
||||
| Italics | Bold | Code |
|
||||
| --------- | -------- | ------ |
|
||||
| _italics_ | **bold** | `code` |
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
| Italics | Bold | Code |
|
||||
| --------- | -------- | ------ |
|
||||
| _italics_ | **bold** | `code` |
|
||||
|
||||
## Code Blocks
|
||||
|
||||
### Syntax
|
||||
|
||||
we can use 3 backticks ``` in new line and write snippet and close with 3 backticks on new line and to highlight language specific syntax, write one word of language name after first 3 backticks, for eg. html, javascript, css, markdown, typescript, txt, bash
|
||||
|
||||
````markdown
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Example HTML5 Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
````
|
||||
|
||||
### Output
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Example HTML5 Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Test</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## List Types
|
||||
|
||||
### Ordered List
|
||||
|
||||
#### Syntax
|
||||
|
||||
```markdown
|
||||
1. First item
|
||||
2. Second item
|
||||
3. Third item
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
1. First item
|
||||
2. Second item
|
||||
3. Third item
|
||||
|
||||
### Unordered List
|
||||
|
||||
#### Syntax
|
||||
|
||||
```markdown
|
||||
- List item
|
||||
- Another item
|
||||
- And another item
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
- List item
|
||||
- Another item
|
||||
- And another item
|
||||
|
||||
### Nested list
|
||||
|
||||
#### Syntax
|
||||
|
||||
```markdown
|
||||
- Fruit
|
||||
- Apple
|
||||
- Orange
|
||||
- Banana
|
||||
- Dairy
|
||||
- Milk
|
||||
- Cheese
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
- Fruit
|
||||
- Apple
|
||||
- Orange
|
||||
- Banana
|
||||
- Dairy
|
||||
- Milk
|
||||
- Cheese
|
||||
|
||||
## Other Elements — abbr, sub, sup, kbd, mark
|
||||
|
||||
### Syntax
|
||||
|
||||
```markdown
|
||||
<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.
|
||||
|
||||
H<sub>2</sub>O
|
||||
|
||||
X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
|
||||
|
||||
Press <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd> to end the session.
|
||||
|
||||
Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.
|
||||
|
||||
H<sub>2</sub>O
|
||||
|
||||
X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
|
||||
|
||||
Press <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd> to end the session.
|
||||
|
||||
Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: 'Second post'
|
||||
description: 'Lorem ipsum dolor sit amet'
|
||||
pubDate: 'Jul 15 2022'
|
||||
heroImage: '/blog-placeholder-4.jpg'
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
|
||||
|
||||
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
|
||||
|
||||
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
|
||||
|
||||
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
|
||||
|
||||
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: 'Third post'
|
||||
description: 'Lorem ipsum dolor sit amet'
|
||||
pubDate: 'Jul 22 2022'
|
||||
heroImage: '/blog-placeholder-2.jpg'
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
|
||||
|
||||
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
|
||||
|
||||
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
|
||||
|
||||
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
|
||||
|
||||
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
title: 'Using MDX'
|
||||
description: 'Lorem ipsum dolor sit amet'
|
||||
pubDate: 'Jun 01 2024'
|
||||
heroImage: '/blog-placeholder-5.jpg'
|
||||
---
|
||||
|
||||
This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
|
||||
|
||||
## Why MDX?
|
||||
|
||||
MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.
|
||||
|
||||
If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.
|
||||
|
||||
## Example
|
||||
|
||||
Here is how you import and use a UI component inside of MDX.
|
||||
When you open this page in the browser, you should see the clickable button below.
|
||||
|
||||
import HeaderLink from '../../components/HeaderLink.astro';
|
||||
|
||||
<HeaderLink href="#" onclick="alert('clicked!')">
|
||||
Embedded component in MDX
|
||||
</HeaderLink>
|
||||
|
||||
## More Links
|
||||
|
||||
- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
|
||||
- [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
|
||||
- **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.
|
||||
1
src/icons/github-mark-white.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 960 B |
4
src/icons/rss.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff9d00" viewBox="0 0 16 16">
|
||||
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
|
||||
<path d="M5.5 12a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-3-8.5a1 1 0 0 1 1-1c5.523 0 10 4.477 10 10a1 1 0 1 1-2 0 8 8 0 0 0-8-8 1 1 0 0 1-1-1zm0 4a1 1 0 0 1 1-1 6 6 0 0 1 6 6 1 1 0 1 1-2 0 4 4 0 0 0-4-4 1 1 0 0 1-1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
@@ -1,85 +1,28 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import BaseHead from '../components/BaseHead.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import FormattedDate from '../components/FormattedDate.astro';
|
||||
import Main from './Main.astro';
|
||||
import { parseTime, printTime } from '../utils/time';
|
||||
|
||||
type Props = CollectionEntry<'blog'>['data'];
|
||||
|
||||
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
|
||||
const { title, author, summary, pubDate, updatedDate } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={title} description={description} />
|
||||
<style>
|
||||
main {
|
||||
width: calc(100% - 2em);
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
}
|
||||
.hero-image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
.prose {
|
||||
width: 720px;
|
||||
max-width: calc(100% - 2em);
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
color: rgb(var(--gray-dark));
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 1em;
|
||||
padding: 1em 0;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.title h1 {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
.date {
|
||||
margin-bottom: 0.5em;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
.last-updated-on {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<article>
|
||||
<div class="hero-image">
|
||||
{heroImage && <img width={1020} height={510} src={heroImage} alt="" />}
|
||||
</div>
|
||||
<div class="prose">
|
||||
<div class="title">
|
||||
<div class="date">
|
||||
<FormattedDate date={pubDate} />
|
||||
{
|
||||
updatedDate && (
|
||||
<div class="last-updated-on">
|
||||
Last updated on <FormattedDate date={updatedDate} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<h1>{title}</h1>
|
||||
<hr />
|
||||
</div>
|
||||
<Main title={title} description={summary}>
|
||||
<article class="m-5 mt-3">
|
||||
<header class="lg:grid grid-cols-[auto_auto_minmax(300px,_1fr)] grid-rows-[auto_auto]">
|
||||
<h2 class="font-bold row-span-2 text-2xl m-2 mt-3">{title}</h2>
|
||||
<address class="post-meta inline-block col-start-2">{author}</address>
|
||||
<time datetime={parseTime(pubDate).toString()}
|
||||
class="post-meta inline-block col-start-3"
|
||||
>{printTime(parseTime(pubDate))}</time>
|
||||
<div class="italic tracking-[3px] text-emph-fg col-start-2 col-span-2 m-2 mt-0">{summary}</div>
|
||||
{updatedDate && <div>
|
||||
Amended <time datetime={parseTime(updatedDate).toString()}>{printTime(parseTime(updatedDate))}</time>
|
||||
</div>}
|
||||
</header>
|
||||
<hr class="mb-3">
|
||||
<main class="max-w-[100ch] font-prose post-content">
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
</article>
|
||||
</Main>
|
||||
|
||||
61
src/layouts/Html.astro
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import '../styles/global.css';
|
||||
import { SITE_TITLE } from '../consts';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
const { title, description, image } = Astro.props;
|
||||
---
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={SITE_TITLE}
|
||||
href={new URL('rss.xml', Astro.site)}
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<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=Montserrat:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
{image &&<meta property="og:image" content={new URL(image, Astro.url)} /> }
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
{image &&<meta property="twitter:image" content={new URL(image, Astro.url)} />}
|
||||
|
||||
<slot name="head" />
|
||||
</head>
|
||||
<body>
|
||||
<slot/>
|
||||
</body>
|
||||
</html>
|
||||
67
src/layouts/Main.astro
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
import NavLink from "../components/NavLink.astro";
|
||||
import Layout from "../layouts/Html.astro"
|
||||
import "../styles/global.css";
|
||||
import GhLogo from "../icons/github-mark-white.svg";
|
||||
import RssLogo from "../icons/rss.svg";
|
||||
import { SITE_DESCRIPTION, SITE_TITLE } from "../consts";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = SITE_TITLE,
|
||||
description = SITE_DESCRIPTION,
|
||||
image,
|
||||
} = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} image={image}>
|
||||
<Fragment slot="head">
|
||||
<meta name="theme-color" content="#222" />
|
||||
<link rel="icon" type="image/png" href="https://github.com/lbfalvy.png"/>
|
||||
</Fragment>
|
||||
<div class="
|
||||
w-screen h-screen
|
||||
flex flex-col
|
||||
md:grid
|
||||
md:grid-rows-[1fr_auto] md:grid-cols-[min-content_auto]
|
||||
">
|
||||
<header class="
|
||||
emph-bg whitespace-nowrap md:px-3 py-2 flex flex-col
|
||||
text-right
|
||||
">
|
||||
<h1 class="font-bold text-2xl m-2 mr-0 text-wrap lg:text-nowrap">Lawrence Bet…</h1>
|
||||
<nav class="gutter
|
||||
flex md:flex-col
|
||||
text-center md:text-right">
|
||||
<NavLink href="/" zone="/blog">Blog</NavLink>
|
||||
<NavLink href="/projects">Projects</NavLink>
|
||||
<NavLink href="/about">About me</NavLink>
|
||||
<NavLink href="/fortune">Fortune</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
<main id="scroll-area" class="md:row-start-1 md:col-start-2 md:row-span-2 md:overflow-y-auto">
|
||||
<slot />
|
||||
</main>
|
||||
<footer class="md:row-start-2 emph-bg flex justify-center gap-3 py-3">
|
||||
<a href="https://github.com/lbfalvy"><Image height="20" width="20" src={GhLogo} alt="Github"/></a>
|
||||
<a href="/rss.xml"><Image height="20" width="20" src={RssLogo} alt="Rss"/></a>
|
||||
</footer>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
const key = `scroll:${window.location.pathname}`;
|
||||
const scrollArea = document.getElementById("scroll-area");
|
||||
scrollArea?.addEventListener('scrollend', () => {
|
||||
sessionStorage.setItem(key, scrollArea.scrollTop.toString())
|
||||
})
|
||||
const savedPos = sessionStorage.getItem(key);
|
||||
if (savedPos) scrollArea!.scrollTop = Number.parseFloat(savedPos);
|
||||
</script>
|
||||
7
src/pages/404.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Main from "../layouts/Main.astro";
|
||||
---
|
||||
|
||||
<Main>
|
||||
404 - page not found
|
||||
</Main>
|
||||
@@ -1,62 +1,40 @@
|
||||
---
|
||||
import Layout from '../layouts/BlogPost.astro';
|
||||
import Main from "../layouts/Main.astro";
|
||||
|
||||
---
|
||||
|
||||
<Layout
|
||||
<Main
|
||||
title="About Me"
|
||||
description="Lorem ipsum dolor sit amet"
|
||||
pubDate={new Date('August 08 2021')}
|
||||
heroImage="/blog-placeholder-about.jpg"
|
||||
description="Lawrence Bethlenfalvy, software engineer"
|
||||
>
|
||||
<div class="max-w-[80ch] m-5">
|
||||
<img src='https://github.com/lbfalvy.png'
|
||||
class="rounded-full h-30 float-right m-3 [shape-outside:_ellipse()]" />
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo
|
||||
viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam
|
||||
adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus
|
||||
et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus
|
||||
vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque
|
||||
sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
|
||||
My name is Lawrence Bethlenfalvy, I make websites and web-based applications primarily
|
||||
with React. I enjoy seeing the fruit of my labour, which is why I do so much
|
||||
frontend development even though I'm not exactly an artistic genius, as finest
|
||||
demonstrated by this website.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non
|
||||
tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non
|
||||
blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna
|
||||
porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis
|
||||
massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc.
|
||||
Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis
|
||||
bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra
|
||||
massa massa ultricies mi.
|
||||
I really like perfectly designed infrastructure, and in an effort to
|
||||
construct it for my own projects I've racked up a considerable amount of
|
||||
DevOps experience.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl
|
||||
suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet
|
||||
nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae
|
||||
turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem
|
||||
dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat
|
||||
semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus
|
||||
vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum
|
||||
facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam
|
||||
vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla
|
||||
urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
|
||||
I studied a lot of advanced mathematical topics in high school, and although
|
||||
I was never a big fan of solving equations for hours on end the approach and
|
||||
some of the concepts stuck with me. I like to draw on mathematical techniques
|
||||
for day-to-day problem solving, but I also view it as a hobby.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper
|
||||
viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc
|
||||
scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur
|
||||
gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus
|
||||
pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim
|
||||
blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id
|
||||
cursus metus aliquam eleifend mi.
|
||||
More recently I've been hard at work on my small conceptual language, Orchid,
|
||||
which, like anything I do, tries to be unopinionated, minimalistic in design,
|
||||
and robust in execution.
|
||||
Designing a programming language is a constant goat game against the halting problem
|
||||
so realising all these principles at the same time is nigh impossible, and incremental
|
||||
progress is fundamentally incompatible with the optimal, holistic approach to
|
||||
issues like type checking, but when I have something to report I do it here.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta
|
||||
nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam
|
||||
tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci
|
||||
ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar
|
||||
proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
|
||||
</p>
|
||||
</Layout>
|
||||
</div>
|
||||
</Main>
|
||||
|
||||
@@ -19,3 +19,8 @@ const { Content } = await render(post);
|
||||
<BlogPost {...post.data}>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
|
||||
<style>
|
||||
@import url('https://unpkg.com/css.gg@2.0.0/icons/css/link.css');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap');
|
||||
</style>
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
import BaseHead from '../../components/BaseHead.astro';
|
||||
import Header from '../../components/Header.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
|
||||
import { getCollection } from 'astro:content';
|
||||
import FormattedDate from '../../components/FormattedDate.astro';
|
||||
|
||||
const posts = (await getCollection('blog')).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
|
||||
<style>
|
||||
main {
|
||||
width: 960px;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ul li {
|
||||
width: calc(50% - 1rem);
|
||||
}
|
||||
ul li * {
|
||||
text-decoration: none;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
ul li:first-child {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
ul li:first-child img {
|
||||
width: 100%;
|
||||
}
|
||||
ul li:first-child .title {
|
||||
font-size: 2.369rem;
|
||||
}
|
||||
ul li img {
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
ul li a {
|
||||
display: block;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
color: rgb(var(--black));
|
||||
line-height: 1;
|
||||
}
|
||||
.date {
|
||||
margin: 0;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
ul li a:hover h4,
|
||||
ul li a:hover .date {
|
||||
color: rgb(var(--accent));
|
||||
}
|
||||
ul a:hover img {
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
ul {
|
||||
gap: 0.5em;
|
||||
}
|
||||
ul li {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
ul li:first-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ul li:first-child .title {
|
||||
font-size: 1.563em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<section>
|
||||
<ul>
|
||||
{
|
||||
posts.map((post) => (
|
||||
<li>
|
||||
<a href={`/blog/${post.id}/`}>
|
||||
<img width={720} height={360} src={post.data.heroImage} alt="" />
|
||||
<h4 class="title">{post.data.title}</h4>
|
||||
<p class="date">
|
||||
<FormattedDate date={post.data.pubDate} />
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,50 +1,11 @@
|
||||
---
|
||||
import BaseHead from '../components/BaseHead.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
import { getCollection } from "astro:content";
|
||||
import Main from "../layouts/Main.astro";
|
||||
import BlogList from "../components/BlogList.svelte";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main>
|
||||
<h1>🧑🚀 Hello, Astronaut!</h1>
|
||||
<p>
|
||||
Welcome to the official <a href="https://astro.build/">Astro</a> blog starter template. This
|
||||
template serves as a lightweight, minimally-styled starting point for anyone looking to build
|
||||
a personal website, blog, or portfolio with Astro.
|
||||
</p>
|
||||
<p>
|
||||
This template comes with a few integrations already configured in your
|
||||
<code>astro.config.mjs</code> file. You can customize your setup with
|
||||
<a href="https://astro.build/integrations">Astro Integrations</a> to add tools like Tailwind,
|
||||
React, or Vue to your project.
|
||||
</p>
|
||||
<p>Here are a few ideas on how to get started with the template:</p>
|
||||
<ul>
|
||||
<li>Edit this page in <code>src/pages/index.astro</code></li>
|
||||
<li>Edit the site header items in <code>src/components/Header.astro</code></li>
|
||||
<li>Add your name to the footer in <code>src/components/Footer.astro</code></li>
|
||||
<li>Check out the included blog posts in <code>src/content/blog/</code></li>
|
||||
<li>Customize the blog post page layout in <code>src/layouts/BlogPost.astro</code></li>
|
||||
</ul>
|
||||
<p>
|
||||
Have fun! If you get stuck, remember to <a href="https://docs.astro.build/"
|
||||
>read the docs
|
||||
</a> or <a href="https://astro.build/chat">join us on Discord</a> to ask questions.
|
||||
</p>
|
||||
<p>
|
||||
Looking for a blog template with a bit more personality? Check out <a
|
||||
href="https://github.com/Charca/astro-blog-template"
|
||||
>astro-blog-template
|
||||
</a> by <a href="https://twitter.com/Charca">Maxi Ferreira</a>.
|
||||
</p>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
<Main>
|
||||
<BlogList posts={posts} client:load />
|
||||
</Main>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection('blog');
|
||||
return rss({
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/blog/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
22
src/pages/rss.xml.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
import type { APIContext } from 'astro';
|
||||
import { parseTime } from '../utils/time';
|
||||
|
||||
export async function GET(context: APIContext) {
|
||||
const posts = await getCollection('blog');
|
||||
return rss({
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
site: context.site!,
|
||||
items: posts.map((post) => ({
|
||||
title: post.data.title.toString(),
|
||||
author: post.data.author,
|
||||
categories: post.data.tags,
|
||||
description: post.data.summary.toString(),
|
||||
pubDate: new Date(parseTime(post.data.pubDate).epochMilliseconds),
|
||||
link: `/blog/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -1,154 +1,114 @@
|
||||
/*
|
||||
The CSS in this style tag is based off of Bear Blog's default CSS.
|
||||
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
|
||||
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
|
||||
*/
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--accent: #2337ff;
|
||||
--accent-dark: #000d8a;
|
||||
--black: 15, 18, 25;
|
||||
--gray: 96, 115, 159;
|
||||
--gray-light: 229, 233, 240;
|
||||
--gray-dark: 34, 41, 57;
|
||||
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
|
||||
--box-shadow: 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
|
||||
0 16px 32px rgba(var(--gray), 33%);
|
||||
background-color: #222;
|
||||
color: #ccc;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Atkinson';
|
||||
src: url('/fonts/atkinson-regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
* {
|
||||
border-color: default;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Atkinson';
|
||||
src: url('/fonts/atkinson-bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
.astro-code {
|
||||
margin: 1ch 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
body {
|
||||
font-family: 'Atkinson', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
background: linear-gradient(var(--gray-gradient)) no-repeat;
|
||||
background-size: 100% 600px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
color: rgb(var(--gray-dark));
|
||||
font-size: 20px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
main {
|
||||
width: 720px;
|
||||
max-width: calc(100% - 2em);
|
||||
margin: auto;
|
||||
padding: 3em 1em;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: rgb(var(--black));
|
||||
line-height: 1.2;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3.052em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2.441em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.953em;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.563em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
strong,
|
||||
b {
|
||||
font-weight: 700;
|
||||
}
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.prose p {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
code {
|
||||
padding: 2px 5px;
|
||||
background-color: rgb(var(--gray-light));
|
||||
border-radius: 2px;
|
||||
}
|
||||
pre {
|
||||
padding: 1.5em;
|
||||
border-radius: 8px;
|
||||
pre, code {
|
||||
background-color: var(--color-side-bg);
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
}
|
||||
pre > code {
|
||||
all: unset;
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid var(--accent);
|
||||
padding: 0 0 0 20px;
|
||||
margin: 0px;
|
||||
font-size: 1.333em;
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgb(var(--gray-light));
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
body {
|
||||
font-size: 18px;
|
||||
}
|
||||
main {
|
||||
padding: 1em;
|
||||
.post-content {
|
||||
a {
|
||||
color: var(--color-link);
|
||||
&:visited {
|
||||
color: var(--color-link-visited);
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: absolute !important;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
|
||||
clip: rect(1px 1px 1px 1px);
|
||||
/* maybe deprecated but we need to support legacy browsers */
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
/* modern browsers, clip-path works inwards from each corner */
|
||||
clip-path: inset(50%);
|
||||
/* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
|
||||
white-space: nowrap;
|
||||
pre {
|
||||
padding-block: .5ch;
|
||||
margin-block: .5ch;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
& i {
|
||||
visibility: hidden;
|
||||
color: #ccc;
|
||||
}
|
||||
&:hover i { visibility: initial; }
|
||||
}
|
||||
h1 { font-size: x-large; }
|
||||
h2 { margin-left: 30px; }
|
||||
h3 {
|
||||
color: #fff7;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& i.gg-link {
|
||||
display: inline-block !important;
|
||||
margin-left: 1ch;
|
||||
margin-right: 1ch;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
#table-of-contents { display: none }
|
||||
#table-of-contents + ul {
|
||||
float: right;
|
||||
background-color: var(--color-side-bg);
|
||||
padding: 5px 12px;
|
||||
width: 20ch;
|
||||
margin: 10px;
|
||||
ul {
|
||||
padding-left: 10px;
|
||||
}
|
||||
li {
|
||||
list-style-type: decimal;
|
||||
list-style-position: inside;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@theme {
|
||||
--tw-border-style: solid;
|
||||
--color-emph-bg: #181818;
|
||||
--color-side-bg: #333;
|
||||
--color-faint-fg: #666;
|
||||
--color-emph-fg: #ddd;
|
||||
--color-shade: #fff1;
|
||||
--color-link: #5c2dc8;
|
||||
--color-link-visited: #746a8b;
|
||||
--font-prose: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.emph-bg {
|
||||
background-color: var(--color-emph-bg);
|
||||
border-color: var(--color-emph-bg);
|
||||
}
|
||||
.post-meta {
|
||||
color: var(--color-faint-fg);
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
.summary {
|
||||
font-style: italic;
|
||||
color: var(---color-emph-fg);
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
.gutter {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
}
|
||||
|
||||
22
src/utils/time.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Temporal } from "@js-temporal/polyfill";
|
||||
|
||||
function lt(one: Temporal.Duration, other: Temporal.DurationLike): boolean {
|
||||
return Temporal.Duration.compare(one, other) < 0
|
||||
}
|
||||
|
||||
export function printTime(time: Temporal.ZonedDateTime): string {
|
||||
const delta = time.until(Temporal.Now.zonedDateTimeISO())
|
||||
if (lt(delta, { minutes: 1 })) return 'now'
|
||||
if (lt(delta, { hours: 1 })) return `${delta.minutes} minutes ago`
|
||||
if (lt(delta, { days: 1 })) return `${delta.hours} hours ago`
|
||||
if (lt(delta, { days: 7 })) return `${delta.round({ smallestUnit: 'days' }).days} days ago`
|
||||
return `at ${time.toPlainDate().toString()} ${time.toPlainTime().toString({ smallestUnit: 'minutes' })}`
|
||||
}
|
||||
|
||||
export function parseTime(string: string): Temporal.ZonedDateTime {
|
||||
return Temporal.ZonedDateTime.from(string)
|
||||
}
|
||||
|
||||
export function isValidTime(string: string): boolean {
|
||||
try { parseTime(string); return true } catch { return false }
|
||||
}
|
||||
5
svelte.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
5
tailwind.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
|
||||
}
|
||||
22
www.lbfalvy.com.code-workspace
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"[markdown][mdx]": {
|
||||
"editor.unicodeHighlight.ambiguousCharacters": false,
|
||||
"editor.unicodeHighlight.invisibleCharacters": false,
|
||||
"diffEditor.ignoreTrimWhitespace": false,
|
||||
"editor.wordWrap": "bounded",
|
||||
"editor.wordWrapColumn": 80,
|
||||
"editor.lineNumbers": "off",
|
||||
"editor.quickSuggestions": {
|
||||
"comments": "off",
|
||||
"strings": "off",
|
||||
"other": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||