说明
在冲浪的时候,发现别人的博客有各种不错的小功能,最近想给博客添加一个github-card功能,所以先从这开始给博客写点小功能吧。
之前使用过hexo主题,他们在实现功能的方式,就是在md的文档中用过花括号作为标记,就可以实现在文档中插入b站的视频,类似如下:
{% dplayer key=value ... %}实现
在Astro中我们也可以有自己的标记来实现类似的功能。
比如这次我们要实现的就是使用如下的方式在md文档中添加github-card功能:
::github{repo="cirry/astro-yi"}接下来说说mdast-util-directive,Astro已经默认集成了它,所以我们不用再去单独安装。
这个包可以帮我们识别md文档中的:,::和:::开头的标记,分别对应的是文本指令,节点指令和容器指令。
本博客主题的旁白标记就是使用容器指令完成了,这次只说节点指令。
export function remarkGithubCard() { const transformer = (tree) => { visit(tree, (node, index, parent) => { // 查找md中的节点指令 if (node.type !== "leafDirective") { return; } // 不为空节点,才能正常渲染需要替换的文本内容 if (!parent || index === undefined) { return; } // 其中的github就是节点的名称,找到指定的节点内容进行替换 if (node.name !== "github") { return; }26 collapsed lines
/** * ::github{repo="cirry/astro"} * 调试需要在 npm run build中的打印信息才能看到节点指令的具体参数 * 类型是leafDirective, * 名称是github * 传入的属性{ repo: 'cirry/astro-yi'} * 没有子节点 * { * type: 'leafDirective', * name: 'github', * attributes: { repo: 'cirry/astro-yi' }, * children: [], * position: { * start: { line: 6, column: 1, offset: 49 }, * end: { line: 6, column: 32, offset: 80 } * } * } */
// ... 省略了大段替换指令文本的代码 }); };
return () => transformer;}将暴露的remarkGithubCard,添加到astro.config.mjs中的remarkPlugins中:
export default defineConfig({ // ... other config markdown:{ remarkPlugins:[...otherPlugins, remarkGithubCard()] }
})拓展
根据以上功能,我们使用类似的方式来实现更多的功能,比如下面的功能等等。
::video[bilibili]{id="xxxxxxx"}::video[youtube]{id="xxxxxx"}附录
import {h as _h, s as _s} from "hastscript";import {visit} from "unist-util-visit";
/** Hacky function that generates an mdast HTML tree ready for conversion to HTML by rehype. */function h(el, attrs = {}, children = []) { const {tagName, properties} = _h(el, attrs); return { type: "paragraph", data: {hName: tagName, hProperties: properties}, children, };}
export function remarkGithubCard() {130 collapsed lines
const transformer = (tree) => { visit(tree, (node, index, parent) => { if (node.type !== "leafDirective") { return; } if (!parent || index === undefined) { return; } if (node.name !== "github") { return; } /** * { * type: 'leafDirective', * name: 'github', * attributes: { repo: 'cirry/astro-yi' }, * children: [], * position: { * start: { line: 6, column: 1, offset: 49 }, * end: { line: 6, column: 32, offset: 80 } * } * } */
const repo = node.attributes.repo ? node.attributes.repo : '' if (!repo || !repo.includes('/')) { return h( 'div', {class: 'hidden'}, 'Invalid repository. ("repo" attributte must be in the format "owner/repo")', ) } const author = repo.split('/')[0] const repoName = repo.split('/')[1]
const cardUuid = `GC${Math.random().toString(36).slice(-6)}` // Collisions are not important
const nAvatar = h(`img#${cardUuid}-avatar`, {class: 'github-avatar mr-4',})
const nTitle = h('div', {class: 'flex items-center justify-between'}, [ h('a', {class: 'flex items-center text-inherit text-xl', href: `https://github.com/${repo}`, target: '_blank',}, [ nAvatar, h('div', {class: ''}, [{type: "text", value: author}]), h('div', {class: 'mx-1'}, [{type: "text", value: '/'}]), h('div', {class: 'font-bold break-all truncate',}, [{type: "text", value: repoName}]), ]), ])
const nDescription = h( `div#${cardUuid}-description`, {class: 'my-2'}, [ {type: "text", value: 'Waiting for api.github.com...',}, ] )
const nStars = h('div', {class: 'flex items-center'}, [ h('i', {class: 'ri-star-line',}, []), h(`div#${cardUuid}-stars`, {class: 'ml-1 mr-4'}, [{type: "text", value: "Waiting"}]) ]) const nForks = h('div', {class: 'flex items-center'}, [ h('i', {class: 'ri-git-fork-line',}, []), h(`div#${cardUuid}-forks`, {class: 'ml-1 mr-4'}, [{type: "text", value: "Waiting"}]) ])
const nLicense = h('div', {class: 'flex items-center'}, [ h('i', {class: 'ri-copyright-line',}, []), h(`div#${cardUuid}-license`, {class: 'ml-1 mr-4'}, [{type: "text", value: "Waiting"}]) ]) const nScript = h( `script#${cardUuid}-script`, {type: 'text/javascript', defer: true}, [ { type: "script", value: ` fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => { if (data.description) { document.getElementById('${cardUuid}-description').innerText = data.description.replace(/:[a-zA-Z0-9_]+:/g, ''); } else { document.getElementById('${cardUuid}-description').innerText = "Description not set" } document.getElementById('${cardUuid}-forks').innerText = data.forks || 0; document.getElementById('${cardUuid}-stars').innerText = data.watchers || 0; const avatarEl = document.getElementById('${cardUuid}-avatar'); avatarEl.setAttribute("src", data.owner.avatar_url) if (data.license?.spdx_id) { document.getElementById('${cardUuid}-license').innerText = data.license?.spdx_id } else { document.getElementById('${cardUuid}-license').innerText = "No License" }; document.getElementById('${cardUuid}-card').classList.remove("fetch-waiting"); console.log("[GITHUB-CARD] Loaded card for ${repo} | ${cardUuid}.") }).catch(err => { const c = document.getElementById('${cardUuid}-card'); c.classList.add("fetch-error"); console.warn("[GITHUB-CARD] (Error) Loading card for ${repo} | ${cardUuid}.") }) `, }] )
// remove(node, (child) => { // if (child.data && "directiveLabel" in child.data && child.data.directiveLabel) { // return true; // } // }); // remove(node,child => { // return true // });
parent.children[index] = h( `div#${cardUuid}-card`, { class: 'shadow w-auto flex flex-col bg-skin-card p-4 my-4 rounded-lg', href: `https://github.com/${repo}`, target: '_blank', repo, }, [ nTitle, nDescription, h('div', {class: 'flex'}, [nStars, nForks, nLicense]), nScript ], ) }); };
return () => transformer;}