diff --git a/package-lock.json b/package-lock.json index 664ec57..791387e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,17 @@ "dependencies": { "@astrojs/node": "^10.0.4", "@astrojs/svelte": "^8.0.4", + "@traptitech/markdown-it-katex": "^3.6.0", "astro": "^6.1.0", "axios": "^1.13.6", + "katex": "^0.16.43", + "markdown-it": "^14.1.1", + "shiki": "^4.0.2", "svelte": "^5.55.0", "typescript": "^5.9.3" }, "devDependencies": { + "@types/markdown-it": "^14.1.2", "@typescript-eslint/parser": "^8.57.2", "eslint": "^10.1.0", "eslint-plugin-astro": "^1.6.0", @@ -1855,6 +1860,15 @@ "vite": "^6.3.0 || ^7.0.0" } }, + "node_modules/@traptitech/markdown-it-katex": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@traptitech/markdown-it-katex/-/markdown-it-katex-3.6.0.tgz", + "integrity": "sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==", + "license": "MIT", + "dependencies": { + "katex": "^0.16.0" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -1893,6 +1907,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1902,6 +1934,13 @@ "@types/unist": "*" } }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -4109,6 +4148,31 @@ "dev": true, "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.43", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.43.tgz", + "integrity": "sha512-K7NL5JtGrFEglipOAjY4UYA69CnTuNmjArxeXF6+bw7h2OGySUPv6QWRjfb1gmutJ4Mw/qLeBqiROOEDULp4nA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4133,6 +4197,15 @@ "node": ">= 0.8.0" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -4194,6 +4267,35 @@ "source-map-js": "^1.2.1" } }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -4444,6 +4546,12 @@ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5547,6 +5655,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6380,6 +6497,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/ufo": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", diff --git a/package.json b/package.json index 7e11e10..b418348 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,17 @@ "dependencies": { "@astrojs/node": "^10.0.4", "@astrojs/svelte": "^8.0.4", + "@traptitech/markdown-it-katex": "^3.6.0", "astro": "^6.1.0", "axios": "^1.13.6", + "katex": "^0.16.43", + "markdown-it": "^14.1.1", + "shiki": "^4.0.2", "svelte": "^5.55.0", "typescript": "^5.9.3" }, "devDependencies": { + "@types/markdown-it": "^14.1.2", "@typescript-eslint/parser": "^8.57.2", "eslint": "^10.1.0", "eslint-plugin-astro": "^1.6.0", diff --git a/src/assets/style.css b/src/assets/style.css index 570b09f..7ce43b2 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -5,20 +5,32 @@ /* == 主题色 == */ :root { - color-scheme: light dark; + color-scheme: light dark; - --color-bg-0: light-dark(oklch(95% 0 0), oklch(30% 0.02 270)); - --color-fg-0: light-dark(oklch(25% 0.02 270), oklch(90% 0.02 270)); + --color-bg-0: light-dark(oklch(95% 0 0), oklch(30% 0.02 270)); + --color-fg-0: light-dark(oklch(25% 0.02 270), oklch(90% 0.02 270)); - --color-link: light-dark(oklch(40% 0.2 270), oklch(80% 0.2 270)); + --color-link: light-dark(oklch(40% 0.2 270), oklch(80% 0.2 270)); } /* == 页面设置 == */ html, body { - margin: 0; - padding: 0; + margin: 0; + padding: 0; - background-color: var(--color-bg-0); - color: var(--color-fg-0); + background-color: var(--color-bg-0); + color: var(--color-fg-0); +} + +/* == 支持 Shiki 的高亮 == */ +pre.shiki code { + font-family: var(--font-mono); + font-size: .9rem; +} + +pre.shiki, +pre.shiki span { + color: light-dark(var(--shiki-light), var(--shiki-dark)); + background-color: light-dark(var(--shiki-light-bg), var(--shiki-dark-bg)); } diff --git a/src/layout/BaseLayout.astro b/src/layout/BaseLayout.astro index 61aeb92..91d1035 100644 --- a/src/layout/BaseLayout.astro +++ b/src/layout/BaseLayout.astro @@ -1,6 +1,7 @@ --- import '../assets/style.css' import '../assets/fonts.css' +import 'katex/dist/katex.min.css' interface Props { title?: string diff --git a/src/lib/apis/legacy/blog.ts b/src/lib/apis/legacy/blog.ts new file mode 100644 index 0000000..e6f4448 --- /dev/null +++ b/src/lib/apis/legacy/blog.ts @@ -0,0 +1,35 @@ +import { legacyClient } from '../clients' + +export const listBlogs = async ({ + page = 1, + limit = 20, +}: { + page?: number + limit?: number +}) => { + const resp = await legacyClient.post('/v1/blog/list', { page, limit }) + return resp.data.data as { + id: number + title: string + featured_image: null | { + image_url: string + } + }[] +} + +export const getBlog: ( + blog_id: number, +) => Promise = async ( + blog_id: number, +) => { + const resp = await legacyClient.get(`/v1/blog/${blog_id}`, { + validateStatus: (status) => status == 200 || status == 404, + }) + if (resp.status == 404) { + return null + } + return resp.data as { + title: string + content: string + } +} diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts new file mode 100644 index 0000000..0f8ebf0 --- /dev/null +++ b/src/lib/markdown.ts @@ -0,0 +1,61 @@ +import MarkdownIt from 'markdown-it' +import mdKatex from '@traptitech/markdown-it-katex' +import { createHighlighter, type Highlighter } from 'shiki' + +let highlighter: Highlighter | null = null + +/** + * Markdown 渲染函数。 + * + * 为了让客户端和服务端表现相同,所以单独拿出来了一个 Markdown 渲染器。 + */ +export async function renderMarkdown(content: string): Promise { + if (highlighter === null) { + highlighter = await createHighlighter({ + themes: ['one-light', 'one-dark-pro'], + /* 额...要我自己定义需要的所有语言吗 */ + langs: [ + 'python', + 'javascript', + 'typescript', + 'typst', + 'markdown', + 'json', + 'toml', + 'yaml', + 'bash', + 'c', + 'c++', + 'rust', + 'go', + 'zig', + 'makefile', + 'make', + 'nim', + 'nix', + 'kdl', + 'md', + 'sh', + ], + }) + } + + const md = new MarkdownIt({ + html: true, + linkify: true, + highlight: (code, lang) => { + return highlighter!.codeToHtml(code, { + lang: lang || 'text', + themes: { + light: 'one-light', + dark: 'one-dark-pro', + }, + defaultColor: false, + }) + }, + }) + + md.use(mdKatex) + + return md.render(content) +} diff --git a/src/pages/blogs/[blog_id].astro b/src/pages/blogs/[blog_id].astro new file mode 100644 index 0000000..6da6822 --- /dev/null +++ b/src/pages/blogs/[blog_id].astro @@ -0,0 +1,23 @@ +--- +import { getBlog } from '../../lib/apis/legacy/blog' +import BaseLayout from '../../layout/BaseLayout.astro' +import { renderMarkdown } from '../../lib/markdown' + +export const prerender = false + +const { blog_id } = Astro.params +const blog_id_num = parseInt(blog_id) + +if (isNaN(blog_id_num) || blog_id_num < 0) { + return Astro.redirect('/404') +} + +const blogData = await getBlog(blog_id_num) +if (blogData === null) { + return Astro.redirect('/404') +} + +const blogRendered = await renderMarkdown(blogData.content) +--- + +