Commit c061264
Changed files (13)
src
components
contents
_lib
templates
integrations
layouts
pages
plugins
types
src/contents/_lib/templates/footnote.typ
@@ -1,7 +1,7 @@
#import "@preview/bullseye:0.1.0": show-target
#let side-footnote-html(body) = {
- show footnote: it => {
+ show footnote: it => if (sys.inputs.at("x-renderer", default: "astro-typst") == "tybios") { it } else {
let number = counter(footnote).display(it.numbering)
let fn-id = "fn-" + number
let ref-id = "fnref-" + number
src/contents/_lib/templates/raw.typ
@@ -3,7 +3,10 @@
#import "@preview/zebraw:0.6.1": zebraw
#let raw-template-html(body) = {
- show: show-target(html: hypraw.with(dedup-styles: false, line-numbers: true, copy-button: false))
+ show: it => if (sys.inputs.at("x-renderer", default: "astro-typst") == "tybios") { it } else {
+ show: hypraw.with(dedup-styles: false, line-numbers: true, copy-button: false)
+ it
+ }
body
}
src/integrations/extract-image.ts
@@ -1,10 +1,10 @@
-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";
+import { getBase64ImageInfo } from "../utils/image.js";
// TODO: After Typst 0.15 published, use jsx and path to auto optimize images in typst
// See https://github.com/typst/typst/pull/7555
@@ -13,7 +13,7 @@ import * as cheerio from "cheerio";
export const extractImage = defineIntegration({
name: "tybios-extract-image",
optionsSchema: z.object({
- dir: z.string().optional().default("assets/images/typst"),
+ dir: z.string().optional().default("assets/img/typ"),
}),
setup({ options }) {
return {
@@ -44,20 +44,12 @@ export const extractImage = defineIntegration({
const el = articleImages[i];
const src = $(el).attr("src") || "";
- const match = src.match(/^data:image\/([^;]+);base64,(.+)$/);
- if (!match) {
+ const imageInfo = getBase64ImageInfo(src);
+ if (!imageInfo) {
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}`;
+ const { filename, base64Data } = imageInfo;
if (!seenImages.has(filename)) {
seenImages.add(filename);
@@ -85,6 +77,11 @@ export const extractImage = defineIntegration({
);
},
},
+ api: {
+ get dir() {
+ return options.dir;
+ },
+ },
};
},
});
src/layouts/GlobalLayout.astro
@@ -43,6 +43,13 @@ const {
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
+ <link
+ rel="alternate"
+ type="application/atom+xml"
+ title="Atom Feed"
+ href="/feed.atom"
+ />
+
<link
rel="stylesheet"
href="https://fred-wang.github.io/MathFonts/NewComputerModern/mathfonts.css"
src/pages/feed.atom.ts
@@ -0,0 +1,53 @@
+import { getCollection } from "astro:content";
+import { Feed } from "feed";
+import { SITE_CONFIG } from "@/config";
+import { rehypeExternalLinks } from "@/plugins/rehype-external-links";
+import { renderTypst } from "@/utils/render-typst";
+import { rehypeExtractImage } from "@/plugins/rehype-extract-image";
+
+export async function GET() {
+ const posts = await getCollection("posts");
+ const siteUrl = SITE_CONFIG.website;
+
+ const feed = new Feed({
+ title: SITE_CONFIG.title,
+ description: SITE_CONFIG.description,
+ id: siteUrl,
+ link: siteUrl,
+ language: "en",
+ favicon: `${siteUrl}/favicon.ico`,
+ copyright: `All rights reserved ${new Date().getFullYear()}`,
+ feedLinks: {
+ rss: `${siteUrl}/feed.atom`,
+ },
+ author: {
+ name: SITE_CONFIG.author,
+ },
+ });
+
+ await Promise.all(
+ posts.map(async (post) => {
+ const url = `${siteUrl}/posts/${post.id}`;
+ if (typeof post.filePath !== "string") return;
+
+ feed.addItem({
+ title: post.data.title,
+ id: url,
+ link: url,
+ description: post.data.desc,
+ content: await renderTypst(post.filePath, [
+ rehypeExtractImage,
+ rehypeExternalLinks,
+ ]),
+ author: [{ name: post.data.author || SITE_CONFIG.author }],
+ date: post.data.date,
+ });
+ }),
+ );
+
+ return new Response(feed.atom1(), {
+ headers: {
+ "Content-Type": "application/xml",
+ },
+ });
+}
src/plugins/rehype-extract-image.ts
@@ -0,0 +1,19 @@
+import type { RehypePlugin } from "@astrojs/markdown-remark";
+import { visit } from "unist-util-visit";
+import { getBase64ImageInfo } from "../utils/image.js";
+import { extractImage } from "@/integrations/extract-image.js";
+import { SITE_CONFIG } from "@/config.js";
+
+export const rehypeExtractImage: RehypePlugin = () => (tree) => {
+ visit(tree, "element", (node) => {
+ if (node.tagName === "img") {
+ const src = node.properties?.src;
+ if (typeof src === "string") {
+ const imageInfo = getBase64ImageInfo(src);
+ if (imageInfo) {
+ node.properties.src = `/${extractImage({ dir: SITE_CONFIG.assetDir }).api.dir}/${imageInfo.filename}`;
+ }
+ }
+ }
+ });
+};
src/types/config.d.ts
@@ -21,4 +21,8 @@ export type SiteConfig = {
* e.g. `"Etc/UTC"`, `"America/New_York"`, `"Asia/Shanghai"`.
*/
timeZone: string;
+ /**
+ * Output directory of typst images
+ */
+ assetDir?: string;
};
src/utils/image.ts
@@ -0,0 +1,23 @@
+import crypto from "node:crypto";
+
+export function getBase64ImageInfo(
+ src: string,
+): { filename: string; base64Data: string; ext: string } | null {
+ const match = src.match(/^data:image\/([^;]+);base64,(.+)$/);
+ if (!match) {
+ return null;
+ }
+
+ const ext = match[1] === "svg+xml" ? "svg" : match[1];
+ const base64Data = match[2];
+
+ const hash = crypto
+ .createHash("sha256")
+ .update(base64Data)
+ .digest("hex")
+ .slice(0, 12);
+
+ const filename = `${hash}.${ext}`;
+
+ return { filename, base64Data, ext };
+}
src/utils/render-typst.ts
@@ -0,0 +1,35 @@
+import { root } from "astro:config/server";
+import { fileURLToPath } from "node:url";
+import type { RehypePlugins } from "@astrojs/markdown-remark";
+import { NodeCompiler } from "@myriaddreamin/typst-ts-node-compiler";
+import { rehypeIt } from "astro-typst/dist/lib/rehype";
+import type { Root } from "hast";
+import * as cheerio from "cheerio";
+
+const rootPath = fileURLToPath(root);
+const $typst = NodeCompiler.create({
+ workspace: rootPath,
+ inputs: { "x-renderer": "tybios" },
+});
+
+export async function renderTypst(
+ mainfilePath: string,
+ plugins?: RehypePlugins,
+): Promise<string> {
+ const docRes = $typst.compileHtml({ mainFilePath: mainfilePath });
+ if (!docRes.result) {
+ docRes.printDiagnostics();
+ return "";
+ }
+ const doc = docRes.result;
+ const html = $typst.tryHtml(doc);
+ if (!html.result) {
+ html.printDiagnostics();
+ return "";
+ }
+ const hast = html.result.hast() as Root;
+ const rehypeRes = await rehypeIt(hast, plugins || []);
+ const $ = cheerio.load(rehypeRes.toString());
+
+ return $("body").html() || "";
+}
astro.config.ts
@@ -17,7 +17,7 @@ export default defineConfig({
target: "html",
}),
extractImage({
- dir: "assets/images/typst",
+ dir: SITE_CONFIG.assetDir,
}),
],
markdown: {
package.json
@@ -18,12 +18,14 @@
"astro-integration-kit": "^0.19.1",
"astro-typst": "^0.12.3",
"cheerio": "^1.2.0",
+ "feed": "^5.2.0",
"luxon": "^3.7.2",
"tailwindcss": "^4.2.1",
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.6",
+ "@types/hast": "^3.0.4",
"@types/luxon": "^3.7.1",
"@types/node": "^25.4.0"
}
pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
cheerio:
specifier: ^1.2.0
version: 1.2.0
+ feed:
+ specifier: ^5.2.0
+ version: 5.2.0
luxon:
specifier: ^3.7.2
version: 3.7.2
@@ -48,6 +51,9 @@ importers:
'@biomejs/biome':
specifier: ^2.4.6
version: 2.4.6
+ '@types/hast':
+ specifier: ^3.0.4
+ version: 3.0.4
'@types/luxon':
specifier: ^3.7.1
version: 3.7.1
@@ -1157,6 +1163,10 @@ packages:
picomatch:
optional: true
+ feed@5.2.0:
+ resolution: {integrity: sha512-hgH6CCb+7+0c8PBlakI2KubG6R+Rb1MhpNcdvqUXZTBwBHf32piwY255diAkAmkGZ6AWlywOU88AkOgP9q8Rdw==}
+ engines: {node: '>=20', pnpm: '>=10'}
+
flattie@1.1.1:
resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
engines: {node: '>=8'}
@@ -2043,6 +2053,10 @@ packages:
resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==}
engines: {node: '>=4'}
+ xml-js@1.6.11:
+ resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
+ hasBin: true
+
xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
@@ -3105,6 +3119,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
+ feed@5.2.0:
+ dependencies:
+ xml-js: 1.6.11
+
flattie@1.1.1: {}
fontace@0.4.1:
@@ -4408,6 +4426,10 @@ snapshots:
which-pm-runs@1.1.0: {}
+ xml-js@1.6.11:
+ dependencies:
+ sax: 1.5.0
+
xxhash-wasm@1.1.0: {}
yargs-parser@22.0.0: {}