From fcce929d7a6173b0e556e46b3bc2a8137e98fc1c Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Mon, 18 May 2026 18:11:14 -0300 Subject: [PATCH] feat: KaTeX math rendering + image lightbox for articles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schema/config support that #22 (protein essay) and #18 (k8s benchmark note) both depend on. Extracted to a single PR so the content PRs can be reduced to just their prose. KaTeX: - Wire remark-math + rehype-katex into MDX in astro.config.mjs so math renders at build time (no client JS). - Add katex / rehype-katex / remark-math as deps. - Import katex.min.css in global.css with a small dark-theme override block so equations sit in the warm palette instead of black/white. ImageLightbox: - Extract the inline lightbox HTML/CSS/JS into src/components/ui/ImageLightbox.astro — a single component included once in ArticleLayout. Any .article-content figure image now gets click-to-zoom (close on backdrop / X button / Escape, no deps). cspell.json: - Add the names and terms used by both pending content PRs so they pass spell-check after they rebase down to pure content: - K8s benchmark note: journalctl, systemd - Math/LaTeX vocabulary: lightbox, eigencomponents, mathbb, mathbf, mathcal, bigl, bigr - Protein essay names: Schoenberg, Wüthrich, ChimeraX, PIBIC, RMSD, MDE, RCSB, PGDm, Gonçalves, Birgin, Martínez, Raydan, UCSF After this lands, both #22 and #18 can rebase to drop their schema/config commits and become pure prose+figure PRs. --- astro.config.mjs | 4 +- cspell.json | 9 +- package-lock.json | 176 ++++++++++++++++++++------ package.json | 3 + src/components/ui/ImageLightbox.astro | 110 ++++++++++++++++ src/layouts/ArticleLayout.astro | 2 + src/styles/global.css | 10 ++ 7 files changed, 272 insertions(+), 42 deletions(-) create mode 100644 src/components/ui/ImageLightbox.astro diff --git a/astro.config.mjs b/astro.config.mjs index 106490e..b314ecb 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,11 +2,13 @@ import { defineConfig } from 'astro/config'; import mdx from '@astrojs/mdx'; import tailwindcss from '@tailwindcss/vite'; +import remarkMath from 'remark-math'; +import rehypeKatex from 'rehype-katex'; import path from 'path'; export default defineConfig({ site: 'https://viniciusdc.github.io', - integrations: [mdx()], + integrations: [mdx({ remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex] })], devToolbar: { enabled: false }, vite: { plugins: [tailwindcss()], diff --git a/cspell.json b/cspell.json index 0be4e1a..4a9e145 100644 --- a/cspell.json +++ b/cspell.json @@ -10,10 +10,11 @@ "keycloak", "oidc", "oauth", "saml", "jwt", "jwks", "fastapi", "pydantic", "scrapy", "uvicorn", "vinicius", "cerutti", "Ceture", "viniciusdc", "Catarina", "UFSC", - "astro", "tailwind", "shadcn", "fontsource", "lhci", + "astro", "tailwind", "shadcn", "fontsource", "lhci", "lightbox", "woff", "woff2", "oklch", "rgb", "rgba", "hsl", "clamp", "minmax", "prefers", "nebari", "jmespath", "snistrict", + "journalctl", "systemd", "firewalld", "libvirt", "vagrantfile", "syscall", "seccomp", "apparmor", "selinux", "etcd", "kube", "kube-proxy", "coredns", @@ -39,9 +40,11 @@ "semidefinite", "semidefiniteness", "Barzilai", "Borwein", "nonmonotone", "GLL", - "Hessian", "quasi-Newton", + "Hessian", "quasi-Newton", "eigencomponents", + "mathbb", "mathbf", "mathcal", "bigl", "bigr", "NMR", "PDB", "crystallography", - "Gram" + "Gram", "Schoenberg", "Wüthrich", "ChimeraX", "PIBIC", "RMSD", "MDE", "RCSB", + "PGDm", "Gonçalves", "Birgin", "Martínez", "Raydan", "UCSF" ], "ignorePaths": [ "node_modules/**", diff --git a/package-lock.json b/package-lock.json index fa63321..d9c0012 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,9 @@ "@fontsource-variable/lora": "^5.2.8", "@tailwindcss/vite": "^4.3.0", "astro": "^6.3.3", + "katex": "^0.16.47", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", "tailwindcss": "^4.2.4" }, "devDependencies": { @@ -4429,6 +4432,12 @@ "@types/unist": "*" } }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -7713,6 +7722,21 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-from-html": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", @@ -7731,6 +7755,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-from-parse5": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", @@ -8614,6 +8654,31 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "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/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -9249,6 +9314,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", @@ -9631,6 +9715,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", @@ -11356,44 +11459,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11608,6 +11673,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-parse": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", @@ -11686,6 +11770,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", diff --git a/package.json b/package.json index 2aa8348..3297042 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "@fontsource-variable/lora": "^5.2.8", "@tailwindcss/vite": "^4.3.0", "astro": "^6.3.3", + "katex": "^0.16.47", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", "tailwindcss": "^4.2.4" }, "devDependencies": { diff --git a/src/components/ui/ImageLightbox.astro b/src/components/ui/ImageLightbox.astro new file mode 100644 index 0000000..f736508 --- /dev/null +++ b/src/components/ui/ImageLightbox.astro @@ -0,0 +1,110 @@ +--- +/** + * Click-to-zoom modal for figure images inside .article-content. + * + * Drop this component anywhere on a page that uses ArticleLayout — it'll + * find every `.article-content figure img`, give it a zoom cursor, and + * open a full-screen modal with the figcaption on click. + * + * Closes on backdrop click, the close button, or Escape. No dependencies. + */ +--- + + + + + + diff --git a/src/layouts/ArticleLayout.astro b/src/layouts/ArticleLayout.astro index d1ef372..8000e44 100644 --- a/src/layouts/ArticleLayout.astro +++ b/src/layouts/ArticleLayout.astro @@ -1,6 +1,7 @@ --- import Layout from "@/layouts/Layout.astro"; import PageRule from "@/components/ui/PageRule.astro"; +import ImageLightbox from "@/components/ui/ImageLightbox.astro"; interface Props { title: string; @@ -17,6 +18,7 @@ const { title, description, meta, subtitle, backHref, backLabel, nextHref, nextL --- +
diff --git a/src/styles/global.css b/src/styles/global.css index b44f89a..9d2ecbd 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @import "@fontsource-variable/geist"; @import "@fontsource-variable/lora"; +@import "katex/dist/katex.min.css"; @custom-variant dark (&:is(.dark *)); @@ -103,3 +104,12 @@ body::before { opacity: 0.025; } } } + +/* KaTeX — dark-theme overrides so math sits in the warm palette + instead of KaTeX's default black/white. */ +.katex { color: inherit; font-size: 1.05em; } +.katex-display { color: var(--foreground); overflow-x: auto; padding: 0.25em 0; } +.katex-display > .katex { color: var(--foreground); } +.katex .mord, .katex .mrel, .katex .mbin, .katex .mop, +.katex .mopen, .katex .mclose, .katex .mpunct { color: inherit; } +.katex .mfrac .frac-line { border-bottom-color: currentColor; }