| import { defineConfig } from 'astro/config'; |
| import mdx from '@astrojs/mdx'; |
| import mermaid from 'astro-mermaid'; |
| import compressor from 'astro-compressor'; |
| import remarkMath from 'remark-math'; |
| import rehypeKatex from 'rehype-katex'; |
| import remarkFootnotes from 'remark-footnotes'; |
| import rehypeSlug from 'rehype-slug'; |
| import rehypeAutolinkHeadings from 'rehype-autolink-headings'; |
| import rehypeCitation from 'rehype-citation'; |
| |
|
|
| |
| function rehypeCodeCopyAndLabel() { |
| return (tree) => { |
| |
| const visit = (node, parent) => { |
| if (!node || typeof node !== 'object') return; |
| const children = Array.isArray(node.children) ? node.children : []; |
| if (node.tagName === 'pre' && children.some(c => c.tagName === 'code')) { |
| |
| const code = children.find(c => c.tagName === 'code'); |
| const collectClasses = (val) => Array.isArray(val) ? val.map(String) : (typeof val === 'string' ? String(val).split(/\s+/) : []); |
| const fromClass = (names) => { |
| const hit = names.find((n) => /^language-/.test(String(n))); |
| return hit ? String(hit).replace(/^language-/, '') : ''; |
| }; |
| const codeClasses = collectClasses(code?.properties?.className); |
| const preClasses = collectClasses(node?.properties?.className); |
| const candidates = [ |
| code?.properties?.['data-language'], |
| fromClass(codeClasses), |
| node?.properties?.['data-language'], |
| fromClass(preClasses), |
| ]; |
| let lang = candidates.find(Boolean) || ''; |
| const displayLang = lang ? String(lang).toUpperCase() : ''; |
| |
| const countLinesFromShiki = () => { |
| const isLineEl = (el) => el && el.type === 'element' && el.tagName === 'span' && Array.isArray(el.properties?.className) && el.properties.className.includes('line'); |
| const hasNonWhitespaceText = (node) => { |
| if (!node) return false; |
| if (node.type === 'text') return /\S/.test(String(node.value || '')); |
| const kids = Array.isArray(node.children) ? node.children : []; |
| return kids.some(hasNonWhitespaceText); |
| }; |
| const collectLines = (node, acc) => { |
| if (!node || typeof node !== 'object') return; |
| if (isLineEl(node)) acc.push(node); |
| const kids = Array.isArray(node.children) ? node.children : []; |
| kids.forEach((k) => collectLines(k, acc)); |
| }; |
| const lines = []; |
| collectLines(code, lines); |
| const nonEmpty = lines.filter((ln) => hasNonWhitespaceText(ln)).length; |
| return nonEmpty || 0; |
| }; |
| const countLinesFromText = () => { |
| |
| const extractText = (node) => { |
| if (!node) return ''; |
| if (node.type === 'text') return String(node.value || ''); |
| const kids = Array.isArray(node.children) ? node.children : []; |
| return kids.map(extractText).join(''); |
| }; |
| const raw = extractText(code); |
| if (!raw || !/\S/.test(raw)) return 0; |
| return raw.split('\n').filter(line => /\S/.test(line)).length; |
| }; |
| const lines = countLinesFromShiki() || countLinesFromText(); |
| const isSingleLine = lines <= 1; |
| |
| if (!isSingleLine) { |
| const approxChars = (() => { |
| const extract = (n) => Array.isArray(n?.children) ? n.children.map(extract).join('') : (n?.type === 'text' ? String(n.value||'') : ''); |
| return extract(code).length; |
| })(); |
| if (approxChars < 6) { |
| |
| |
| node.__forceSingle = true; |
| } |
| } |
| |
| code.properties = code.properties || {}; |
| if (displayLang) code.properties['data-language'] = displayLang; |
| |
| const wrapper = { |
| type: 'element', |
| tagName: 'div', |
| properties: { className: ['code-card'].concat((isSingleLine || node.__forceSingle) ? ['no-copy'] : []), 'data-language': displayLang }, |
| children: (isSingleLine || node.__forceSingle) ? [ node ] : [ |
| { |
| type: 'element', |
| tagName: 'button', |
| properties: { className: ['code-copy', 'button--ghost'], type: 'button', 'aria-label': 'Copy code' }, |
| children: [ |
| { |
| type: 'element', |
| tagName: 'svg', |
| properties: { viewBox: '0 0 24 24', 'aria-hidden': 'true', focusable: 'false' }, |
| children: [ |
| { type: 'element', tagName: 'path', properties: { d: 'M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z' }, children: [] } |
| ] |
| } |
| ] |
| }, |
| node |
| ] |
| }; |
| if (parent && Array.isArray(parent.children)) { |
| const idx = parent.children.indexOf(node); |
| if (idx !== -1) parent.children[idx] = wrapper; |
| } |
| return; |
| } |
| children.forEach((c) => visit(c, node)); |
| }; |
| visit(tree, null); |
| }; |
| } |
|
|
| export default defineConfig({ |
| output: 'static', |
| integrations: [ |
| mermaid({ theme: 'forest', autoTheme: true }), |
| mdx(), |
| |
| compressor({ brotli: false, gzip: true }) |
| ], |
| devToolbar: { |
| enabled: false |
| }, |
| markdown: { |
| shikiConfig: { |
| themes: { |
| light: 'github-light', |
| dark: 'github-dark' |
| }, |
| defaultColor: false, |
| wrap: false, |
| langAlias: { |
| |
| mdx: 'tsx' |
| } |
| }, |
| remarkPlugins: [ |
| remarkMath, |
| [remarkFootnotes, { inlineNotes: true }] |
| ], |
| rehypePlugins: [ |
| rehypeSlug, |
| [rehypeAutolinkHeadings, { behavior: 'wrap' }], |
| rehypeKatex, |
| [rehypeCitation, { |
| bibliography: 'src/content/bibliography.bib', |
| linkCitations: true |
| }], |
| rehypeCodeCopyAndLabel |
| ] |
| } |
| }); |
|
|
|
|
|
|