Commit 5c53bc5
Changed files (8)
src
contents
_lib
components
posts
integrations
styles
article
src/contents/_lib/components/frame.typ
@@ -0,0 +1,12 @@
+#import "@preview/bullseye:0.1.0": target
+
+/// Same to `#html.frame`, but it will return content if the target is not html.
+///
+/// - content (content):
+/// -> content
+#let frame(content) = context {
+ if target() == "html" {
+ return html.frame(content)
+ }
+ return content
+}
src/contents/_lib/tybios.typ
@@ -1,3 +1,4 @@
+#import "components/frame.typ": frame
#import "components/fullwidth.typ": fullwidth
#import "components/marginnote.typ": marginnote
src/contents/posts/test.typ
@@ -72,6 +72,25 @@ And some inline code: `print("Hello, World!")`.
Maybe you want Typst's special inline code with syntax highlighting: ```rust println!("Hello, World!"); ```
+= Frame
+
+You can use ```typ #tybios.frame()``` wrap any content that can't automatically convert to HTML, convert them to SVGs.
+#footnote[Converted SVGs will adapt to dark theme by color invert and hue rotation, but the result may not be perfect.]
+This function is a wrapper of #link("https://typst.app/docs/reference/html/frame/", ```typ #html.frame()```),
+but only use it when ```typ #target()``` exists and equals to "html". Otherwise, it will return the content directly without wrapping it in a frame.
+
+#figure(
+ {
+ import "@preview/lilaq:0.5.0" as lq
+ let x = lq.linspace(0, 10)
+
+ tybios.frame(lq.diagram(
+ lq.plot(x, x => calc.sin(x)),
+ ))
+ },
+ caption: [The diagram of the sine function],
+)
+
= List & Enumeration
Example of enumeration:
src/integrations/extract-image.ts
@@ -0,0 +1,90 @@
+import crypto from "node:crypto";
+import fs from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { z } from "astro/zod";
+import { defineIntegration } from "astro-integration-kit";
+import * as cheerio from "cheerio";
+
+// TODO: After Typst 0.15 published, use jsx and path to auto optimize images in typst
+// See https://github.com/typst/typst/pull/7555
+// It's not possible now because image path is string in Typst 0.14,
+// so we can't resolve images' actual path to import them by Astro's `<Image />` component.
+export const extractImage = defineIntegration({
+ name: "tybios-extract-image",
+ optionsSchema: z.object({
+ dir: z.string().optional().default("assets/images/typst"),
+ }),
+ setup({ options }) {
+ return {
+ hooks: {
+ "astro:build:done": async ({ dir, logger }) => {
+ const rootDir = fileURLToPath(dir);
+ const outDir = path.join(rootDir, options.dir);
+ await fs.mkdir(outDir, { recursive: true });
+
+ const seenImages = new Set<string>();
+
+ async function processDirectory(currentDir: string) {
+ const entries = await fs.readdir(currentDir, {
+ withFileTypes: true,
+ });
+ for (const entry of entries) {
+ const fullPath = path.join(currentDir, entry.name);
+ if (entry.isDirectory()) {
+ await processDirectory(fullPath);
+ } else if (entry.isFile() && entry.name.endsWith(".html")) {
+ const html = await fs.readFile(fullPath, "utf-8");
+ const $ = cheerio.load(html);
+ let htmlChanged = false;
+
+ const articleImages = $("article img");
+
+ for (let i = 0; i < articleImages.length; i++) {
+ const el = articleImages[i];
+ const src = $(el).attr("src") || "";
+
+ const match = src.match(/^data:image\/([^;]+);base64,(.+)$/);
+ if (!match) {
+ continue;
+ }
+
+ const ext = match[1] === "svg+xml" ? "svg" : match[1];
+ const base64Data = match[2];
+
+ const hash = crypto
+ .createHash("sha256")
+ .update(base64Data)
+ .digest("hex")
+ .slice(0, 10);
+ const filename = `${hash}.${ext}`;
+
+ if (!seenImages.has(filename)) {
+ seenImages.add(filename);
+ const imageBuffer = Buffer.from(base64Data, "base64");
+ await fs.writeFile(
+ path.join(outDir, filename),
+ imageBuffer,
+ );
+ }
+
+ $(el).attr("src", `/${options.dir}/${filename}`);
+ htmlChanged = true;
+ }
+
+ if (htmlChanged) {
+ await fs.writeFile(fullPath, $.html());
+ }
+ }
+ }
+ }
+
+ await processDirectory(rootDir);
+ logger.info(
+ `Extracted ${seenImages.size} base64 images to ${options.dir}`,
+ );
+ },
+ },
+ };
+ },
+});
src/styles/article/index.css
@@ -46,4 +46,10 @@ article,
article {
@apply text-base lg:text-lg;
@apply max-w-full md:max-w-2/3;
+
+ img.typst-frame {
+ @variant dark {
+ filter: invert(88%) hue-rotate(180deg);
+ }
+ }
}
astro.config.ts
@@ -1,7 +1,8 @@
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "astro/config";
-import { SITE_CONFIG } from "./src/config";
import { typst } from "astro-typst";
+import { SITE_CONFIG } from "./src/config";
+import { extractImage } from "./src/integrations/extract-image";
import { rehypeCleanEndnotes } from "./src/plugins/rehype-clean-endnotes";
// https://astro.build/config
@@ -14,6 +15,9 @@ export default defineConfig({
},
target: "html",
}),
+ extractImage({
+ dir: "assets/images/typst",
+ }),
],
markdown: {
rehypePlugins: [rehypeCleanEndnotes],
package.json
@@ -15,13 +15,16 @@
"@myriaddreamin/typst.ts": "0.7.0-rc2",
"@tailwindcss/vite": "^4.2.1",
"astro": "^6.0.1",
+ "astro-integration-kit": "^0.19.1",
"astro-typst": "^0.12.3",
+ "cheerio": "^1.2.0",
"luxon": "^3.7.2",
"tailwindcss": "^4.2.1",
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.6",
- "@types/luxon": "^3.7.1"
+ "@types/luxon": "^3.7.1",
+ "@types/node": "^25.4.0"
}
}
pnpm-lock.yaml
@@ -22,13 +22,19 @@ importers:
version: 0.7.0-rc2(@myriaddreamin/typst-ts-renderer@0.7.0-rc2)
'@tailwindcss/vite':
specifier: ^4.2.1
- version: 4.2.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
+ version: 4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1))
astro:
specifier: ^6.0.1
- version: 6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)
+ version: 6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)
+ astro-integration-kit:
+ specifier: ^0.19.1
+ version: 0.19.1(astro@6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))
astro-typst:
specifier: ^0.12.3
- version: 0.12.3(@myriaddreamin/typst-ts-node-compiler@0.7.0-rc2)(@myriaddreamin/typst-ts-renderer@0.7.0-rc2)(@myriaddreamin/typst.ts@0.7.0-rc2(@myriaddreamin/typst-ts-renderer@0.7.0-rc2))(astro@6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))(cheerio@1.2.0)(rollup@4.59.0)
+ version: 0.12.3(@myriaddreamin/typst-ts-node-compiler@0.7.0-rc2)(@myriaddreamin/typst-ts-renderer@0.7.0-rc2)(@myriaddreamin/typst.ts@0.7.0-rc2(@myriaddreamin/typst-ts-renderer@0.7.0-rc2))(astro@6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))(cheerio@1.2.0)(rollup@4.59.0)
+ cheerio:
+ specifier: ^1.2.0
+ version: 1.2.0
luxon:
specifier: ^3.7.2
version: 3.7.2
@@ -45,6 +51,9 @@ importers:
'@types/luxon':
specifier: ^3.7.1
version: 3.7.1
+ '@types/node':
+ specifier: ^25.4.0
+ version: 25.4.0
packages:
@@ -863,6 +872,9 @@ packages:
'@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
+ '@types/node@25.4.0':
+ resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==}
+
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -1853,6 +1865,9 @@ packages:
uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
undici@7.22.0:
resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==}
engines: {node: '>=20.18.1'}
@@ -2668,12 +2683,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
- '@tailwindcss/vite@4.2.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))':
+ '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1))':
dependencies:
'@tailwindcss/node': 4.2.1
'@tailwindcss/oxide': 4.2.1
tailwindcss: 4.2.1
- vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
+ vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)
'@types/debug@4.1.12':
dependencies:
@@ -2703,6 +2718,10 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/node@25.4.0':
+ dependencies:
+ undici-types: 7.18.2
+
'@types/resolve@1.20.2': {}
'@types/unist@2.0.11': {}
@@ -2730,19 +2749,19 @@ snapshots:
astring@1.9.0: {}
- astro-integration-kit@0.19.1(astro@6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)):
+ astro-integration-kit@0.19.1(astro@6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)):
dependencies:
- astro: 6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)
+ astro: 6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)
pathe: 1.1.2
- astro-typst@0.12.3(@myriaddreamin/typst-ts-node-compiler@0.7.0-rc2)(@myriaddreamin/typst-ts-renderer@0.7.0-rc2)(@myriaddreamin/typst.ts@0.7.0-rc2(@myriaddreamin/typst-ts-renderer@0.7.0-rc2))(astro@6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))(cheerio@1.2.0)(rollup@4.59.0):
+ astro-typst@0.12.3(@myriaddreamin/typst-ts-node-compiler@0.7.0-rc2)(@myriaddreamin/typst-ts-renderer@0.7.0-rc2)(@myriaddreamin/typst.ts@0.7.0-rc2(@myriaddreamin/typst-ts-renderer@0.7.0-rc2))(astro@6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))(cheerio@1.2.0)(rollup@4.59.0):
dependencies:
'@myriaddreamin/typst-ts-node-compiler': 0.7.0-rc2
'@myriaddreamin/typst-ts-renderer': 0.7.0-rc2
'@myriaddreamin/typst.ts': 0.7.0-rc2(@myriaddreamin/typst-ts-renderer@0.7.0-rc2)
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0)
- astro: 6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)
- astro-integration-kit: 0.19.1(astro@6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))
+ astro: 6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3)
+ astro-integration-kit: 0.19.1(astro@6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))
cheerio: 1.2.0
rehype-stringify: 10.0.1
typstx: 0.0.1-beta.1
@@ -2751,7 +2770,7 @@ snapshots:
- rollup
- supports-color
- astro@6.0.1(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3):
+ astro@6.0.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3):
dependencies:
'@astrojs/compiler': 3.0.0
'@astrojs/internal-helpers': 0.8.0
@@ -2803,8 +2822,8 @@ snapshots:
unist-util-visit: 5.1.0
unstorage: 1.17.4
vfile: 6.0.3
- vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
- vitefu: 1.1.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
+ vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)
+ vitefu: 1.1.2(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1))
xxhash-wasm: 1.1.0
yargs-parser: 22.0.0
zod: 4.3.6
@@ -4269,6 +4288,8 @@ snapshots:
uncrypto@0.1.3: {}
+ undici-types@7.18.2: {}
+
undici@7.22.0: {}
unified@11.0.5:
@@ -4359,7 +4380,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
- vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1):
+ vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1):
dependencies:
esbuild: 0.27.3
fdir: 6.5.0(picomatch@4.0.3)
@@ -4368,13 +4389,14 @@ snapshots:
rollup: 4.59.0
tinyglobby: 0.2.15
optionalDependencies:
+ '@types/node': 25.4.0
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.31.1
- vitefu@1.1.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)):
+ vitefu@1.1.2(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)):
optionalDependencies:
- vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
+ vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)
web-namespaces@2.0.1: {}