Commit c8f92be
Changed files (7)
src
components
utils
styles
utils
src/components/utils/Markdown.astro
@@ -1,108 +1,12 @@
---
import '@/styles/markdown.css';
-import Button from '@components/widgets/Button.astro';
-import { isFirstInstance } from '@utils/component-utils';
-import { Icon } from 'astro-icon/components';
import type { HTMLAttributes } from 'astro/types';
type Props = HTMLAttributes<'article'>;
const { class: className, ...rest } = Astro.props;
-
-const html = await Astro.slots.render('default');
-const hasPre = /<pre[\s>]/i.test(html);
-// 开发环境下,保存文件后 isFirstInstance 会返回 false,所以需要强制显示工具栏
-const firstHasPre = hasPre && (isFirstInstance('md-has-pre', Astro.url) || import.meta.env.DEV);
---
<article class={className} {...rest}>
- <Fragment set:html={html} />
+ <slot />
</article>
-
-{
- firstHasPre && (
- <template
- id="code-toolbar-template"
- class="code-block-wrapper collapse-open collapse my-4 w-[initial]"
- >
- <div class="bg-primary/60 text-primary-content z-10 flex w-full items-center justify-between">
- <Button class="toggle-btn btn-ghost btn-primary rounded-bl-none">
- <Icon
- name="material-symbols:keyboard-arrow-down-rounded"
- class="h-5 w-5 duration-300"
- />
- </Button>
- <span class="language font-mono text-sm" />
- <Button class="copy-btn btn-ghost btn-primary rounded-br-none">
- <Icon name="material-symbols:file-copy-rounded" class="h-5 w-5 duration-300" />
- </Button>
- </div>
- <div class="collapse-content w-full !p-0" />
- </template>
- )
-}
-
-<script>
- async function init() {
- prettierPreCode();
- }
-
- function prettierPreCode() {
- const codeBlocks = document.querySelectorAll('article pre');
- const template = document.getElementById('code-toolbar-template') as HTMLTemplateElement;
- if (!template) return;
-
- const wrapPre = (pre: HTMLPreElement) => {
- const language = (pre?.getAttribute('data-language') || 'plaintext').toLocaleUpperCase();
-
- const wrapper = document.createElement('div');
- wrapper.className = template.className;
-
- const toolbar = template.content.cloneNode(true) as DocumentFragment;
- const langLabel = toolbar.querySelector('.language');
- if (langLabel) {
- langLabel.textContent = language;
- }
-
- pre.parentNode?.insertBefore(wrapper, pre);
- wrapper.appendChild(toolbar);
- wrapper.querySelector('.collapse-content')?.appendChild(pre);
-
- const toggleBtn = wrapper.querySelector('.toggle-btn');
- const copyBtn = wrapper.querySelector('.copy-btn');
-
- toggleBtn?.addEventListener('click', () => {
- toggleBtn.querySelector('svg')?.classList.toggle('-rotate-90');
- wrapper.classList.toggle('collapse-open');
- });
-
- const code = pre.querySelector('code');
- copyBtn?.addEventListener('click', async () => {
- try {
- const text = code?.textContent || '';
- await navigator.clipboard.writeText(text);
- copyBtn?.classList.add('text-success');
- setTimeout(() => {
- copyBtn?.classList.remove('text-success');
- }, 600);
- } catch (err) {
- console.error('Failed to copy:', err);
- copyBtn?.classList.add('text-error');
- setTimeout(() => {
- copyBtn?.classList.remove('text-error');
- }, 600);
- }
- });
- };
-
- const isWrapped = (pre: HTMLPreElement) => {
- return pre.parentElement?.classList.contains('collapse-content');
- };
-
- codeBlocks.forEach((pre) => {
- if (!isWrapped(pre as HTMLPreElement)) wrapPre(pre as HTMLPreElement);
- });
- }
-
- document.addEventListener('astro:page-load', init);
-</script>
src/plugins/rehype-prettier-codes.ts
@@ -0,0 +1,56 @@
+import { wrapperTagName } from './shiki-transformers';
+import type { RehypePlugin } from '@astrojs/markdown-remark';
+import type { ElementContent } from 'hast';
+import { visit } from 'unist-util-visit';
+
+export const rehypePrettierCodes: RehypePlugin = function () {
+ return (tree) => {
+ visit(tree, 'element', (node) => {
+ if (node.tagName !== wrapperTagName) return;
+ node.tagName = 'div';
+ const originalClass = (node.properties?.class as string) || '';
+ const additionalClass = ['group'];
+ const newClass = originalClass.split(' ').concat(additionalClass).join(' ');
+ node.properties = { ...node.properties, class: newClass };
+
+ const language = node.properties?.dataLanguage as string;
+ const languageTag: ElementContent = {
+ type: 'element',
+ tagName: 'span',
+ properties: {
+ class:
+ 'badge badge-outline absolute top-2 right-2 group-hover:opacity-0 duration-200',
+ },
+ children: [
+ {
+ type: 'text',
+ value: language,
+ },
+ ],
+ };
+
+ const copyIcon: ElementContent = {
+ type: 'element',
+ tagName: 'span',
+ properties: {
+ class: 'icon-[material-symbols--file-copy-rounded]',
+ },
+ children: [],
+ };
+ const copyBtn: ElementContent = {
+ type: 'element',
+ tagName: 'button',
+ properties: {
+ class:
+ 'badge badge-outline tooltip tooltip-left absolute top-2 right-3 group-hover:opacity-100 duration-200 opacity-0',
+ onclick: `navigator.clipboard.writeText(this.parentElement.children[0].textContent);
+ this.dataset.tip = 'Copied!'; setTimeout(() => this.dataset.tip = 'Copy', 1000);`,
+ 'data-tip': 'Copy',
+ },
+ children: [copyIcon],
+ };
+
+ node.children.push(languageTag, copyBtn);
+ });
+ };
+};
src/plugins/shiki-transformers.ts
@@ -0,0 +1,15 @@
+import { h } from 'hastscript';
+import type { ShikiTransformer } from 'shiki';
+
+export const wrapperTagName = 'code-block';
+
+export const wrapCode = (): ShikiTransformer => {
+ return {
+ name: 'shiki-transformer-wrap-code',
+ pre(node) {
+ const container = h('pre', node.children);
+ node.children = [container];
+ node.tagName = wrapperTagName;
+ },
+ };
+};
src/styles/markdown.css
@@ -168,14 +168,12 @@ article {
/* 代码样式 */
pre {
- padding: 0.5rem;
+ padding: 1rem 2rem 1rem 0.5rem;
counter-reset: line;
transition: 300ms all ease;
+ overflow-x: scroll;
code {
- white-space: pre-wrap;
- overflow-wrap: anywhere;
- word-break: break-all;
display: flex;
flex-direction: column;
gap: 0.125em;
@@ -201,6 +199,14 @@ article {
}
}
+ div:has(> pre) {
+ @apply relative rounded-md;
+
+ > :last-child > span {
+ @apply bg-base-content/60!;
+ }
+ }
+
code:not(pre code) {
padding: 0 0.25rem;
src/utils/component-utils.ts
@@ -1,12 +0,0 @@
-import type { AstroGlobal } from 'astro';
-
-const renderedInstance = new Set<string>();
-
-export function isFirstInstance(id: string, url: AstroGlobal['url']): boolean {
- const key = `${id}-${url.pathname}`;
- if (renderedInstance.has(key)) {
- return false;
- }
- renderedInstance.add(key);
- return true;
-}
astro.config.mjs
@@ -1,14 +1,18 @@
// @ts-check
import { CDN } from './src/constants/cdn.mjs';
+import { rehypePrettierCodes } from './src/plugins/rehype-prettier-codes.ts';
import { rehypeWrapTables } from './src/plugins/rehype-wrap-tables.ts';
import { remarkExcerpt } from './src/plugins/remark-excerpt.ts';
import { remarkGithubBlockquote } from './src/plugins/remark-github-blockquote.ts';
// import { remarkHeadingShift } from './src/plugins/remark-heading-shift.ts';
import { remarkImageProcess } from './src/plugins/remark-image-process.ts';
import { remarkReadingTime } from './src/plugins/remark-reading-time.ts';
+import { wrapCode } from './src/plugins/shiki-transformers.ts';
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
+import { transformerNotationDiff } from '@shikijs/transformers';
+import { transformerNotationHighlight } from '@shikijs/transformers';
import tailwindcss from '@tailwindcss/vite';
import icon from 'astro-icon';
import pagefind from 'astro-pagefind';
@@ -36,6 +40,7 @@ export default defineConfig({
dark: 'one-dark-pro',
},
defaultColor: false,
+ transformers: [transformerNotationDiff(), transformerNotationHighlight(), wrapCode()],
},
remarkPlugins: [
// remarkHeadingShift,
@@ -69,6 +74,7 @@ export default defineConfig({
},
],
rehypeWrapTables,
+ rehypePrettierCodes,
],
},
vite: {
package.json
@@ -20,6 +20,7 @@
"@iconify-json/ic": "^1.2.2",
"@iconify-json/material-symbols": "^1.2.14",
"@iconify-json/mdi": "^1.2.3",
+ "@shikijs/transformers": "^2.3.2",
"@tailwindcss/vite": "^4.0.6",
"astro": "^5.2.5",
"astro-compress": "2.3.5",
@@ -43,7 +44,8 @@
"sharp": "^0.33.5",
"tailwindcss": "^4.0.6",
"typescript": "^5.7.3",
- "unist-util-visit": "^5.0.0"
+ "unist-util-visit": "^5.0.0",
+ "shiki": "^2.3.2"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",