说明
在冲浪的时候,发现别人的博客有各种不错的小功能,最近想给博客添加一个github-card功能,所以先从这开始给博客写点小功能吧。
之前使用过hexo主题,他们在实现功能的方式,就是在md的文档中用过花括号作为标记,就可以实现在文档中插入b站的视频,类似如下:
1{% dplayer key=value ... %}
实现
在Astro中我们也可以有自己的标记来实现类似的功能。
比如这次我们要实现的就是使用如下的方式在md文档中添加github-card功能:
1::github{repo="cirry/astro-yi"}
接下来说说mdast-util-directive,Astro已经默认集成了它,所以我们不用再去单独安装。
这个包可以帮我们识别md文档中的:
,::
和:::
开头的标记,分别对应的是文本指令,节点指令和容器指令。
本博客主题的旁白标记就是使用容器指令完成了,这次只说节点指令。
1export function remarkGithubCard() {2 const transformer = (tree) => {3 visit(tree, (node, index, parent) => {4 // 查找md中的节点指令5 if (node.type !== "leafDirective") {6 return;7 }8 // 不为空节点,才能正常渲染需要替换的文本内容9 if (!parent || index === undefined) {10 return;11 }12 // 其中的github就是节点的名称,找到指定的节点内容进行替换13 if (node.name !== "github") {14 return;15 }26 collapsed lines
16
17 /**18 * ::github{repo="cirry/astro"}19 * 调试需要在 npm run build中的打印信息才能看到节点指令的具体参数20 * 类型是leafDirective,21 * 名称是github22 * 传入的属性{ repo: 'cirry/astro-yi'}23 * 没有子节点24 * {25 * type: 'leafDirective',26 * name: 'github',27 * attributes: { repo: 'cirry/astro-yi' },28 * children: [],29 * position: {30 * start: { line: 6, column: 1, offset: 49 },31 * end: { line: 6, column: 32, offset: 80 }32 * }33 * }34 */35
36 // ... 省略了大段替换指令文本的代码37 });38 };39
40 return () => transformer;41}
将暴露的remarkGithubCard,添加到astro.config.mjs中的remarkPlugins中:
1export default defineConfig({2 // ... other config3 markdown:{4 remarkPlugins:[...otherPlugins, remarkGithubCard()]5 }6
7})
拓展
根据以上功能,我们使用类似的方式来实现更多的功能,比如下面的功能等等。
1::video[bilibili]{id="xxxxxxx"}2::video[youtube]{id="xxxxxx"}
附录
1import {h as _h, s as _s} from "hastscript";2import {visit} from "unist-util-visit";3
4/** Hacky function that generates an mdast HTML tree ready for conversion to HTML by rehype. */5function h(el, attrs = {}, children = []) {6 const {tagName, properties} = _h(el, attrs);7 return {8 type: "paragraph",9 data: {hName: tagName, hProperties: properties},10 children,11 };12}13
14
15export function remarkGithubCard() {130 collapsed lines
16 const transformer = (tree) => {17 visit(tree, (node, index, parent) => {18 if (node.type !== "leafDirective") {19 return;20 }21 if (!parent || index === undefined) {22 return;23 }24 if (node.name !== "github") {25 return;26 }27 /**28 * {29 * type: 'leafDirective',30 * name: 'github',31 * attributes: { repo: 'cirry/astro-yi' },32 * children: [],33 * position: {34 * start: { line: 6, column: 1, offset: 49 },35 * end: { line: 6, column: 32, offset: 80 }36 * }37 * }38 */39
40 const repo = node.attributes.repo ? node.attributes.repo : ''41 if (!repo || !repo.includes('/')) {42 return h(43 'div',44 {class: 'hidden'},45 'Invalid repository. ("repo" attributte must be in the format "owner/repo")',46 )47 }48 const author = repo.split('/')[0]49 const repoName = repo.split('/')[1]50
51 const cardUuid = `GC${Math.random().toString(36).slice(-6)}` // Collisions are not important52
53 const nAvatar = h(`img#${cardUuid}-avatar`, {class: 'github-avatar mr-4',})54
55
56 const nTitle = h('div', {class: 'flex items-center justify-between'}, [57 h('a', {class: 'flex items-center text-inherit text-xl', href: `https://github.com/${repo}`, target: '_blank',}, [58 nAvatar,59 h('div', {class: ''}, [{type: "text", value: author}]),60 h('div', {class: 'mx-1'}, [{type: "text", value: '/'}]),61 h('div', {class: 'font-bold break-all truncate',}, [{type: "text", value: repoName}]),62 ]),63 ])64
65 const nDescription = h(66 `div#${cardUuid}-description`,67 {class: 'my-2'}, [68 {type: "text", value: 'Waiting for api.github.com...',},69 ]70 )71
72 const nStars = h('div', {class: 'flex items-center'}, [73 h('i', {class: 'ri-star-line',}, []),74 h(`div#${cardUuid}-stars`, {class: 'ml-1 mr-4'}, [{type: "text", value: "Waiting"}])75 ])76 const nForks = h('div', {class: 'flex items-center'}, [77 h('i', {class: 'ri-git-fork-line',}, []),78 h(`div#${cardUuid}-forks`, {class: 'ml-1 mr-4'}, [{type: "text", value: "Waiting"}])79 ])80
81 const nLicense = h('div', {class: 'flex items-center'}, [82 h('i', {class: 'ri-copyright-line',}, []),83 h(`div#${cardUuid}-license`, {class: 'ml-1 mr-4'}, [{type: "text", value: "Waiting"}])84 ])85 const nScript = h(86 `script#${cardUuid}-script`,87 {type: 'text/javascript', defer: true},88 [89 {90 type: "script", value: `91 fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => {92 if (data.description) {93 document.getElementById('${cardUuid}-description').innerText = data.description.replace(/:[a-zA-Z0-9_]+:/g, '');94 } else {95 document.getElementById('${cardUuid}-description').innerText = "Description not set"96 }97 document.getElementById('${cardUuid}-forks').innerText = data.forks || 0;98 document.getElementById('${cardUuid}-stars').innerText = data.watchers || 0;99 const avatarEl = document.getElementById('${cardUuid}-avatar');100 avatarEl.setAttribute("src", data.owner.avatar_url)101 if (data.license?.spdx_id) {102 document.getElementById('${cardUuid}-license').innerText = data.license?.spdx_id103 } else {104 document.getElementById('${cardUuid}-license').innerText = "No License"105 };106 document.getElementById('${cardUuid}-card').classList.remove("fetch-waiting");107 console.log("[GITHUB-CARD] Loaded card for ${repo} | ${cardUuid}.")108 }).catch(err => {109 const c = document.getElementById('${cardUuid}-card');110 c.classList.add("fetch-error");111 console.warn("[GITHUB-CARD] (Error) Loading card for ${repo} | ${cardUuid}.")112 }) `,113 }]114 )115
116 // remove(node, (child) => {117 // if (child.data && "directiveLabel" in child.data && child.data.directiveLabel) {118 // return true;119 // }120 // });121 // remove(node,child => {122 // return true123 // });124
125
126 parent.children[index] = h(127 `div#${cardUuid}-card`,128 {129 class: 'shadow w-auto flex flex-col bg-skin-card p-4 my-4 rounded-lg',130 href: `https://github.com/${repo}`,131 target: '_blank',132 repo,133 },134 [135 nTitle,136 nDescription,137 h('div', {class: 'flex'}, [nStars, nForks, nLicense]),138 nScript139 ],140 )141 });142 };143
144 return () => transformer;145}