实现效果:
博客美化:代码块美化示例 - Ygria’s Blog
偶然看到ray.so的开源工具网站:
Create beautiful images of your code,提供了非常漂亮的代码图片生成功能,支持一系列主题,并能对代码块进行高亮、添加背景并导出成图片。
正好最近在折腾我的Hugo静态博客,索性就把这个功能集成了进来。配合Hugo官方提供的render-codeblock hook,可以无痛兼容之前的博客文章。
Hugo render code hook
参考Hugo博客官方给出的文档:
Code block render hooks | Hugo,我们可以劫持Markdown文件生成html的过程,用自己的代码段代替。
在主题文件夹下(如果没有主题,则在根目录下),新建文件render-codeblock.html
(注意文件名一定不能弄错,否则会无法解析到)
your-hugo-site/
├── themes/
└── themename/
├── layouts/
└── _default/
└── _markup/
└── render-codeblock.html
在配置文件中,添加:
# Hugo 解析文档的配置
[markup]
# 语法高亮设置 (https://gohugo.io/content-management/syntax-highlighting)
[markup.highlight]
noClasses = false
这样我们就可以编写自己的渲染逻辑了。
render-codeblock模版结构
通过检查元素,容易得到页面结构为:
wrapper 最外层)
├── background(根据传入参数,判断是否显示)
├── window(代码窗口)
├── header
│ └── controls 仿apple窗口的三个圆点
│ └── fileName 文件名
│ └── 操作button,支持复制代码和导出图片
└── code(代码块)
在这个页面可以通过.Type
拿到传入的语言(即写markdown语法时,```后紧邻的声明),并能通过类似于```{title=“rust demo” theme=“candy” mode=“dark” padding=“16”}语法,传入attribute map并在模版代码中解析。通过这种方法,我们能够更灵活地应用代码块并渲染不同的样式。
自定义高亮
主题声明
通过查看ray.so源码得知使用了
shiki 进行高亮,并自定义了若干主题。在assets/js下新建一个名为hightlighter.js的文件
并导入shiki:
需要将这个js文件引入到Hugo模版代码中。由于使用了import语法,需要声明type = “module”
{{ $hightlightJS := resources.Get "js/hightlight.js" | resources.Minify | resources.Fingerprint }}
<script src="{{ $hightlightJS.Permalink }}" defer type = "module"></script>
将ray.so中声明的主题列表THEMES拷贝过来(在app/(navigation)/(code)/store/themes.ts
中:)
通过阅读源码,得知是通过动态替换CSS Variable的值来渲染主题的,因此我们只需要声明一个主题,即css-variables主题。
const shikiTheme = createCssVariablesTheme({
name: "css-variables",
variablePrefix: "--ray-",
variableDefaults: {},
fontStyle: true,
});
该主题通过读取当前作用域内的CSS Variables的值进行代码高亮。
声明用于高亮的hightlighter
使用单例,声明hightlighter。这里我做了单例声明,这样如果页面上有很多代码块,可以避免重复创建多次,并预先将页面上所有代码块使用的语言遍历出来,获取hightlighter。
const ShikiHightlighter = (function () {
let highlight = null;
async function init(languages) {
// Singleton 初始化代码
highlight = await getHighlighter({
langs: languages,
themes: [shikiTheme]
})
return highlight
}
return {
getInstance: function (languages) {
if (!highlight) {
highlight = init(languages);
}
return highlight;
}
};
})();
// after dom loaded
document.querySelectorAll('[id^="code-id-"]').forEach(element => {
console.log(element.dataset.language);
languages.add(element.dataset.language)
});
var highlighter = await ShikiHightlighter.getInstance(Array.from(languages));
页面加载后,执行高亮逻辑
在render-codeblock.html中,将读入的属性放到html元素的CSS属性内,再通过CSS选择器和属性进行获取。格式为data-*
的属性,可以直接通过element.dataset.*
的方式来获取,很方便。
根据传入的theme和mode(light或dark),可以从THEMES对象里拿到CSS Varible属性,将这个属性赋到code wrapper上,这样高亮出的效果就和变量一致了。
document.addEventListener("DOMContentLoaded", async function () {
const codeblocks = document.querySelectorAll(".code-wrapper");
// 如果有,则调用hightlight
if (codeblocks) {
const languages= new Set();
document.querySelectorAll('[id^="code-id-"]').forEach(element => {
console.log(element.dataset.language);
languages.add(element.dataset.language)
});
var highlighter = await ShikiHightlighter.getInstance(Array.from(languages));
codeblocks.forEach(async codeblock => {
try {
const codeElement = codeblock.querySelector('[id^="code-"]')
const code = codeElement.textContent;
const language = codeElement.dataset.language;
const variables = THEMES[codeElement.dataset.theme]['syntax'][codeblock.dataset.theme]
const styleVariables = Object.keys(variables).map(key => `${key}: ${variables[key]};`).join(' ');
codeblock.style = styleVariables;
console.log("data-language :" + language); // 输出: "python"
// 使用shiki进行代码高亮
const highlightedCode = await highlighter.codeToHtml(code, {
lang: language,
theme: "css-variables"
})
codeElement.innerHTML = highlightedCode;
} catch (error) {
console.error("hightlight failed...", error)
}
})
}
}
)
均使用Attribute传入,通过CSS实现。
值得一提的是darkmode的写法,通过变量的形式,变化变量颜色。这样样式就不用写两遍了~
:root {
--ray-highlight-hover: rgba(0, 0, 0, 0.05);
--ray-highlight: rgba(0, 0, 0, 0.1);
--ray-highlight-border: transparent;
--line-number: rgba(0, 0, 0, 0.2);
}
[data-theme="dark"] {
--frame-highlight-border: rgba(255, 255, 255, 0.3);
--ray-highlight-hover: rgba(255, 255, 255, 0.05);
--ray-highlight: rgba(255, 255, 255, 0.1);
--line-number: rgba(255, 255, 255, 0.2);
}
.window {
display: flex;
box-shadow: 0 0 0 1px var(--frame-highlight-border), 0 0 0 1.5px var(--frame-shadow-border), 0 2.8px 2.2px rgba(0, 0, 0, 0.034), 0 6.7px 5.3px rgba(0, 0, 0, 0.048), 0 12.5px 10px rgba(0, 0, 0, 0.06), 0 22.3px 17.9px rgba(0, 0, 0, 0.072), 0 41.8px 33.4px rgba(0, 0, 0, 0.086), 0 100px 80px rgba(0, 0, 0, 0.12);
background: var(--frame-background);
}
其他功能
复制代码
逻辑:在header中加入两个元素,一个图标为复制,一个为复制成功(隐藏)。监听按钮点击事件,点击后,向剪贴板写入文本,并将复制按钮隐藏,显示复制成功按钮。
三秒后,重新显示复制按钮。
<button id="copyButton-{{ $id }}">
<svg class="lucide lucide-copy" />
</button>
<button id="copySuccess-{{ $id }}" style="display: none;" >
<svg class="lucide lucide-clipboard-check" />
</button>
const copyButton = codeblock.querySelector('[id^="copyButton-"]');
const copySuccess = document.querySelector('[id^="copySuccess-"]')
// 复制代码
copyButton.addEventListener("click", () => {
navigator.clipboard.writeText(code)
copyButton.style.display = 'none';
copySuccess.style.display = "block"
setTimeout(() => {
copyButton.style.display = 'block';
copySuccess.style.display = "none"
}, 3000)
})
导出图片
使用html2canvas库。需在baseof.html中引入:
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
为了美观,导出时将复制、导出等按钮隐藏了,等下载好了再显示。
// 导出成图片
const controlButtons = codeblock.querySelector('[id^="controls-button-"]');
const exportImageButton = codeblock.querySelector('[id^="exportImage-"]');
exportImageButton.addEventListener("click", async () => {
{ {/* const wrapper = document.querySelector() */ } }
controlButtons.style.visibility = 'hidden';
html2canvas(codeblock).then(canvas => {
// 添加 canvas 到 body,可选
{ {/* document.body.appendChild(canvas); */ } }
// 保存为图片
var img = canvas.toDataURL("image/png");
// 创建一个链接元素用于下载
var link = document.createElement('a');
link.download = 'my-image.png';
link.href = img;
link.click();
controlButtons.style.visibility = 'visible';
});
})
总结
通过看源码,学习到了很多关于CSS变量的知识。原生HTML、JS、CSS也是很强大的~框架能做,原生基本也能做。