7 Commits

Author SHA1 Message Date
6156b512e9 Merge pull request 'dev' (#4) from dev into main
Reviewed-on: #4
build 1.1.2 version
2025-05-13 00:22:22 +08:00
9e5d98e2e2 🆗 增加shikijs踩坑记录blog
🆗 调整markdown代码块copy success的图标颜色
🆗 去除首页blog图片的左边距
2025-05-13 00:06:48 +08:00
101db5aa83 增加copy按钮添加时改的shikijs包代码说明 2025-05-12 18:57:01 +08:00
9ef5139d93 代码块添加复制按钮 2025-05-12 18:51:21 +08:00
f999f0912e 添加网易云音乐vip歌曲的自建backup服务地址 2025-05-12 16:08:05 +08:00
75cd6ce315 Merge branch 'dev' of code.2ha.me:dev.2ha.me/dev.2ha.me into dev 2025-05-09 15:53:04 +08:00
fe3978ddbf 添加缺少的gif资源 2025-05-09 15:53:01 +08:00
10 changed files with 563 additions and 49 deletions

View File

@@ -18,6 +18,7 @@ import remarkToc from 'remark-toc'
import sectionize from '@hbsnow/rehype-sectionize'
import { transformerNotationSkip } from './src/lib/transformerNotationSkip'
import { transformerDiffHighlight } from './src/lib/transformerDiffHighlight'
import { transformerCopyButton } from './src/lib/transformerCopyButton'
import icon from 'astro-icon'
@@ -64,6 +65,11 @@ export default defineConfig({
transformerRenderWhitespace(),
transformerNotationSkip(),
transformerDiffHighlight(),
transformerCopyButton({
duration: 1000,
successIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E",
copyIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E",
})
],
},
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@@ -10,10 +10,11 @@ interface Track {
image: { '#text': string }[] //
url: string
'@attr'?: { nowplaying: string },
outerurl: string
outerurl: string,
backupurl: string
}
const SpotifyPresence = () => {
const Music163Player = () => {
const [displayData, setDisplayData] = useState<Track | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isPlaying, setIsPlaying] = useState(false);
@@ -47,9 +48,10 @@ const SpotifyPresence = () => {
album: {
'#text': lastweekFirstSong.al.name
},
image: [{'#text': lastweekFirstSong.al.picUrl}],
image: [{'#text': lastweekFirstSong.al.picUrl.replace('http:', 'https:')}],
url: 'https://music.163.com/song?id=' + lastweekFirstSong.id,
outerurl: "https://music.163.com/song/media/outer/url?id=" + lastweekFirstSong.id + ".mp3"
outerurl: "https://music.163.com/song/media/outer/url?id=" + lastweekFirstSong.id + ".mp3",
backupurl: "https://f.2ha.me/music/" + lastweekFirstSong.id + ".mp3"
}
setDisplayData(track)
setIsLoading(false)
@@ -82,7 +84,7 @@ const SpotifyPresence = () => {
if (!displayData) return <p>Something absolutely horrible has gone wrong</p>
const { name: song, artist, album, image, url, outerurl } = displayData
const { name: song, artist, album, image, url, outerurl, backupurl } = displayData
return (
<>
@@ -139,7 +141,10 @@ const SpotifyPresence = () => {
<span className="w-[85%] truncate text-xs text-muted-foreground">
<span className="font-semibold text-secondary-foreground">
<div>
<audio ref={audioRef} src={outerurl} />
<audio ref={audioRef} >
<source src={outerurl} type="audio/mp3" />
<source src={backupurl} type="audio/mp3" />
</audio>
</div>
</span>
</span>
@@ -160,4 +165,4 @@ const SpotifyPresence = () => {
)
}
export default SpotifyPresence
export default Music163Player

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View File

@@ -0,0 +1,404 @@
---
title: 'Astro集成Shikijs 和RehypePrettyCode踩坑'
description: '1. shikijs重复依赖打包失败; 2. 使用Rehype Pretty Code/Copy Button 给markdown代码块添加复制按钮时发生错误Cannot read properties of undefined (reading "type")'
date: 2025-05-12
tags: ['typescript', 'astro', 'shiki']
image: 'assets/shikicodecopybutton.png'
authors: ['jimlee']
---
import FileTree from '@/components/starlight/FileTree.astro'
# Shikijs重复依赖导致代码报错打包失败
## 执行 npm run build 后发生以下错误
```log
src/lib/transformerNotationSkip.ts - error ts(2322): Type 'import("/var/jenkins_home/workspace/dev.2ha.me/node_modules/@shikijs/transformers/node_modules/@shikijs/types/dist/index").ShikiTransformer' is not assignable to type 'import("/var/jenkins_home/workspace/dev.2ha.me/node_modules/@shikijs/types/dist/index").ShikiTransformer'.
Types of property 'preprocess' are incompatible.
return createCommentNotationTransformer(
~~~~~~
Result (78 files):
- 1 error
- 0 warnings
- 0 hints
```
## 检查src/lib/transformerNotationSkip.ts代码
这里引用的ShikiTransformer是/node_modules/@shikijs/types 中的 ShikiTransformer, 与 node_modules/@shikijs/transformers/node_modules/@shikijs/types 中的 ShikiTransformer 代码相同,但是引用不同
```typescript
import { type ShikiTransformer } from '@shikijs/types';
import { createCommentNotationTransformer } from '@shikijs/transformers'
export interface TransformerNotationSkipOptions {
/**
* Class for skipped lines
*/
classActiveSkip?: string
/**
* Class added to the root element when the code has skipped lines
*/
classActivePre?: string
}
export function transformerNotationSkip(
options: TransformerNotationSkipOptions = {},
): ShikiTransformer {
const { classActiveSkip = 'skip', classActivePre = undefined } = options
return createCommentNotationTransformer(
'skip-lines',
// comment-start | marker | range | comment-end
/^\s*(?:\/\/|\/\*|<!--|#)\s+\[!code skip:(\d+):(\d+)\]\s*(?:\*\/|-->)?/,
function ([_, start, end], _line) {
_line.children = [{ type: 'text', value: `${start}-${end}` }]
_line.properties = { style: `counter-set:line ${end}` }
if (classActiveSkip) this.addClassToHast(_line, classActiveSkip)
if (classActivePre) this.addClassToHast(this.pre, classActivePre)
return false
},
undefined, // remove empty lines
)
}
```
## 检查安装好依赖后的@shikijs目录
<FileTree>
- node_modules
- @shikijs
- engine-javascript
- dist
- README.md
- package.json
- LICENSE
- types
- dist
- README.md
- package.json
- LICENSE
- langs
- dist
- README.md
- package.json
- LICENSE
- engine-oniguruma
- dist
- README.md
- package.json
- LICENSE
- transformers
- dist
- README.md
- package.json
- node_modules // 重复依赖最外层 node_modules 下的 @shikijs shiki
- shiki // 与 node_modules/shiki 重复
- dist
- README.md
- package.json
- LICENSE
- oniguruma-to-es
- dist
- README.md
- package.json
- types
- LICENSE
- @shikijs // 与 node_modules/@shikijs 重复
- engine-javascript
- types
- engine-oniguruma
- vscode-textmate
- core
- LICENSE
- themes
- dist
- README.md
- package.json
- LICENSE
- vscode-textmate
- dist
- README.md
- package.json
- LICENSE.md
- core
- dist
- README.md
- package.json
- LICENSE
</FileTree>
## 删除node_modules中的重复依赖
```shellscript
rm -rf node_modules/@shikijs/transformers/node_modules/
```
# 给Astro博客Markdown代码块添加复制按钮
通过查找[Rehype Pretty Code文档](https://rehype-pretty.pages.dev/)找到 [Rehype Pretty Code/Copy Button](https://rehype-pretty.pages.dev/plugins/copy-button/)这个实验性功能
## 1. 安装依赖
```shellscript
npm install @rehype-pretty/transformers
```
## 2. 添加transformerCopyButton.ts
这个ts文件是基于node_modules\@rehype-pretty\transformers\dist\copy-button.js修改而来
```typescript
import type { ShikiTransformer } from "shiki";
import { h } from "hastscript";
export interface CopyButtonOptions {
duration?: number;
copyIcon?: string;
successIcon?: string
}
export const transformerCopyButton = (
options: CopyButtonOptions = {
duration: 1000
}
): ShikiTransformer => {
return {
name: 'shiki-transformer-copy-button',
code(node) {
const button = h('button', {
class: 'shiki-transformer-button-copy',
'data-code': this.source,
onclick: `
navigator.clipboard.writeText(this.dataset.code);
this.classList.add('shiki-transformer-button-copied');
setTimeout(() => this.classList.remove('shiki-transformer-button-copied'), ${options.duration})
`
}, [
h('span', { class: 'ready' }),
h('span', { class: 'success' })
]);
node.children.push(button)
node.children.push({
type: 'element',
tagName: 'style',
properties: {},
children: [
{
type: 'text',
value: buttonStyles({
successIcon: options.successIcon,
copyIcon: options.copyIcon
})
}
]
})
}
}
}
function buttonStyles({
successIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M16 3h2.6A2.4 2.4 0 0 1 21 5.4v15.2a2.4 2.4 0 0 1-2.4 2.4H5.4A2.4 2.4 0 0 1 3 20.6V5.4A2.4 2.4 0 0 1 5.4 3H8m0 11l3 3l5-7M8.8 1h6.4a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-.8.8H8.8a.8.8 0 0 1-.8-.8V1.8a.8.8 0 0 1 .8-.8'/%3E%3C/svg%3E",
copyIcon = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20stroke='rgba(128,128,128,1)'%20stroke-linecap='round'%20stroke-linejoin='round'%20stroke-width='2'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='8'%20height='4'%20x='8'%20y='2'%20rx='1'%20ry='1'/%3E%3Cpath%20d='M16%204h2a2%202%200%200%201%202%202v14a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2h2'/%3E%3C/svg%3E",
}: {
successIcon?: string,
copyIcon?: string
}) {
let buttonStyle =
`
:root {
--border-color: #e2e2e3;
--background-color: #f6f6f7;
--hover-background-color: #ffff
}
pre:has(code) {
position: relative;
}
pre button.shiki-transformer-button-copy {
position: absolute;
top: 12px;
right: 12px;
z-index: 3;
border: 1px solid var(--border-color);
border-radius: 4px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
place-items: center;
background-color: var(--background-color);
cursor: pointer;
background-repeat: no-repeat;
transition: var(--border-color) .25s, var(--background-color) .25s, opacity .25s;
&:hover {
background-color: var(--hover-background-color);
}
& span {
width: 100%;
aspect-ratio: 1 / 1;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
& .ready {
width: 20px;
height: 20px;
background-image: url("${copyIcon}");
}
& .success {
display: none;
width: 20px;
height: 20px;
background-image: url("${successIcon}");
}
&.shiki-transformer-button-copied {
& .success {
display: block;
}
& .ready {
display: none;
}
}
}`
return buttonStyle
}
```
## 3. 添加插件
```typescript
+ import { transformerCopyButton } from './src/lib/transformerCopyButton'
export default defineConfig({
site: 'https://dev.2ha.me',
integrations: [
tailwind({
applyBaseStyles: false,
}),
sitemap(),
mdx(),
react(),
icon(),
],
markdown: {
syntaxHighlight: false,
rehypePlugins: [
[
rehypeExternalLinks,
{
target: '_blank',
rel: ['nofollow', 'noreferrer', 'noopener'],
},
],
rehypeHeadingIds,
[
rehypeKatex,
{
strict: false,
},
],
sectionize as any,
[
rehypePrettyCode,
{
theme: {
light: 'everforest-dark',
dark: 'everforest-dark',
},
transformers: [
transformerNotationDiff(),
transformerMetaHighlight(),
transformerRenderWhitespace(),
transformerNotationSkip(),
transformerDiffHighlight(),
+ transformerCopyButton({
+ duration: 1000,
+ successIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E",
+ copyIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E",
+ })
],
},
],
],
remarkPlugins: [remarkToc, remarkMath, remarkEmoji],
},
server: {
port: 1234,
host: true,
},
devToolbar: {
enabled: false,
},
})
```
## 3. 启动项目后访问blog后报错
```log
TypeError: Cannot read properties of undefined (reading 'type')
at /vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:516:22
at Array.flatMap (<anonymous>)
at /vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:507:41
at Array.forEach (<anonymous>)
at Object.root (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:501:21)
at tokensToHast (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/core/dist/index.mjs:1313:33)
at codeToHast (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/core/dist/index.mjs:1188:10… …
```
## 5. 修改node_modules/@shikijs/transformers依赖中的transformerRenderWhitespace方法
```javascript
function transformerRenderWhitespace(options = {}) {
const classMap = {
" ": options.classSpace ?? "space",
" ": options.classTab ?? "tab"
};
const position = options.position ?? "all";
const keys = Object.keys(classMap);
return {
name: "@shikijs/transformers:render-whitespace",
// We use `root` hook here to ensure it runs after all other transformers
root(root) {
const pre = root.children[0];
const code = pre.children[0];
code.children.forEach(
(line) => {
if (line.type !== "element")
return;
const elements = line.children.filter((token) => token.type === "element");
const last = elements.length - 1;
line.children = line.children.flatMap((token) => {
if (token.type !== "element")
return token;
const index = elements.indexOf(token);
if (position === "boundary" && index !== 0 && index !== last)
return token;
if (position === "trailing" && index !== last)
return token;
+ if (token.children.length === 0) {
+ return token;
+ }
const node = token.children[0];
if (node.type !== "text" || !node.value)
return token;
const parts = splitSpaces(
node.value.split(/([ \t])/).filter((i) => i.length),
position === "boundary" && index === last && last !== 0 ? "trailing" : position,
position !== "trailing"
);
if (parts.length <= 1)
return token;
return parts.map((part) => {
const clone = {
...token,
properties: { ...token.properties }
};
clone.children = [{ type: "text", value: part }];
if (keys.includes(part)) {
this.addClassToHast(clone, classMap[part]);
delete clone.properties.style;
}
return clone;
});
});
}
);
}
};
}
```

View File

@@ -0,0 +1,133 @@
import type { ShikiTransformer } from "shiki";
import { h } from "hastscript";
/**
* shikijs transformerRenderWhitespace 会报错 Cannot read properties of undefined (reading 'type'), 解决办法是
* @shikijs/transformers/dist/index.mjs:516:22 加一个判断 提前返回
* 如下
* if (token.children.length === 0) {
return token;
}
在这行代码之前加上面这段代码
const node = token.children[0];
if (node.type !== "text" || !node.value)
return token;
*/
export interface CopyButtonOptions {
duration?: number;
copyIcon?: string;
successIcon?: string
}
export const transformerCopyButton = (
options: CopyButtonOptions = {
duration: 1000
}
): ShikiTransformer => {
return {
name: 'shiki-transformer-copy-button',
code(node) {
const button = h('button', {
class: 'shiki-transformer-button-copy',
'data-code': this.source,
onclick: `
navigator.clipboard.writeText(this.dataset.code);
this.classList.add('shiki-transformer-button-copied');
setTimeout(() => this.classList.remove('shiki-transformer-button-copied'), ${options.duration})
`
}, [
h('span', { class: 'ready' }),
h('span', { class: 'success' })
]);
node.children.push(button)
node.children.push({
type: 'element',
tagName: 'style',
properties: {},
children: [
{
type: 'text',
value: buttonStyles({
successIcon: options.successIcon,
copyIcon: options.copyIcon
})
}
]
})
}
}
}
function buttonStyles({
successIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M16 3h2.6A2.4 2.4 0 0 1 21 5.4v15.2a2.4 2.4 0 0 1-2.4 2.4H5.4A2.4 2.4 0 0 1 3 20.6V5.4A2.4 2.4 0 0 1 5.4 3H8m0 11l3 3l5-7M8.8 1h6.4a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-.8.8H8.8a.8.8 0 0 1-.8-.8V1.8a.8.8 0 0 1 .8-.8'/%3E%3C/svg%3E",
copyIcon = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20stroke='rgba(128,128,128,1)'%20stroke-linecap='round'%20stroke-linejoin='round'%20stroke-width='2'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='8'%20height='4'%20x='8'%20y='2'%20rx='1'%20ry='1'/%3E%3Cpath%20d='M16%204h2a2%202%200%200%201%202%202v14a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2h2'/%3E%3C/svg%3E",
}: {
successIcon?: string,
copyIcon?: string
}) {
let buttonStyle =
`
:root {
--border-color: #e2e2e3;
--background-color: #f6f6f7;
--hover-background-color: #ffff
}
pre:has(code) {
position: relative;
}
pre button.shiki-transformer-button-copy {
position: absolute;
top: 12px;
right: 12px;
z-index: 3;
border: 1px solid var(--border-color);
border-radius: 4px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
place-items: center;
background-color: var(--background-color);
cursor: pointer;
background-repeat: no-repeat;
transition: var(--border-color) .25s, var(--background-color) .25s, opacity .25s;
&:hover {
background-color: var(--hover-background-color);
}
& span {
width: 100%;
aspect-ratio: 1 / 1;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
& .ready {
width: 20px;
height: 20px;
background-image: url("${copyIcon}");
}
& .success {
display: none;
width: 20px;
height: 20px;
background-image: url("${successIcon}");
}
&.shiki-transformer-button-copied {
& .success {
display: block;
}
& .ready {
display: none;
}
}
}`
return buttonStyle
}

View File

@@ -29,7 +29,7 @@ export function transformerNotationSkip(
if (classActivePre) this.addClassToHast(this.pre, classActivePre)
return false
},
false, // remove empty lines
undefined, // remove empty lines
)
}

View File

@@ -143,7 +143,7 @@ const authors = await getCollection('authors')
</TableCell>
<TableCell className="text-right">
<Link href="https://www.bilibili.com/video/BV1462uY8Eo4" target="_blank">
<img class="h-12 w-25" src="/static/images/output.gif" />
<img class="h-12 w-25" src="/static/images/maoliang.gif" />
</Link>
</TableCell>
</TableRow>

View File

@@ -36,46 +36,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
role="img"
aria-label="Introduction"
>
<!-- <div
class="overlay size-full rounded-3xl bg-[url('/static/404.webp')] bg-cover bg-center bg-no-repeat opacity-0 transition-opacity duration-200"
>
</div> -->
<!-- class="first relative flex aspect-square justify-center rounded-3xl border [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto" -->
<!-- {
bgIndex && (<img
class="no-repeat relative w-full max-w-fit justify-center rounded-3xl object-cover"
src="/static/images/tou.gif"
style="filter: contrast(2) saturate(0) invert(100) sepia(100); border-color: #afafaf;"
/>)
}
{
!bgIndex && (
<img
class="no-repeat relative w-full max-w-fit justify-center rounded-3xl object-cover"
src="/static/images/maoliang.gif"
/>)
} -->
<RandomAnimeBackground client:load/>
<!-- <div class="absolute self-end p-2 justify-center">
<FuzzyText
client:load
baseIntensity={0.04}
hoverIntensity={0.24}
fontSize={96}
color="#fbb229"
,>4O4</FuzzyText
>
<div class="mb-3 mt-3"></div>
<FuzzyText
client:load
baseIntensity={0.04}
hoverIntensity={0.24}
fontSize={36}
color="#f95038">Not Found</FuzzyText
>
</div> -->
</div>
<div
@@ -127,7 +88,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
alt={`Featured image for the latest post: ${latestPost.data.title}`}
width={477}
height={251}
class="w-full rounded-2xl border border-border sm:ml-1 sm:w-[82%] "
class="w-full rounded-2xl border border-border sm:w-[82%] "
/>
</Link>

View File

@@ -517,4 +517,9 @@
.bg-color {
background-color: #e9d3b6 !important;
}
.shiki-transformer-button-copy {
border: none !important;
background-color: hsl(0deg 0% 0% / 0%) !important;
}