将Asciinema集成到Docusaurus项目中
在Markdown文档中, 涉及到一些终端操作时, 需要一种方式来展示这些操作.
常见的解决方案
虽然Markdown内置的codeblock语法方便, 但在某些情况下, 它也有不足之处.
比如, 当终端操作步骤过长, 包含大量输入输出内容时, 会导致文档占用过多空间, 影响阅读体验.
另一种方法是将终端操作录制成视频并通过链接引用, 这样视频可以嵌入页面中展示操作过程.
然而, 视频文件通常较大, 占用较多空间, 而终端操作本质上只是字符串.
工具介绍
Asciinema
Asciinema是一个开源的终端录制工具, 可以记录终端操作并生成可播放的文件.
这个文件仅包含文本信息, 不占用太多空间, 非常适合展示终端操作演示.
要生成终端操作演示, 需要安装Asciinema的命令行工具, 并使用该工具进行录制.
详细的安装和使用教程可以参考getting-started页面.
Docusaurus
Docusaurus是一个开源的静态网站生成器, 基于 React 实现, 允许使用Markdown编写文档, 并用
React渲染.
在Docusaurus中, 支持使用MDX扩展.
MDX
MDX是Markdown的扩展, 允许我们在Markdown文档中使用JavaScript
代码来实现更丰富的文档内容渲染.
功能设计
Docusaurus支持MDX语法, 因此我们可以在文档中引入JavaScript代码来渲染页面.
虽然Asciinema提供了用于页面渲染的JavaScript库, 但遗憾的是目前只有基本的JavaScript实现.
由于Docusaurus是基于React实现的, 因此我们需要将Asciinema提供的JavaScript库封装为Docusaurus支持的React
组件.
封装React组件
如果我们需要在Docusaurus中集成Asciinema, 那么需要完成以下步骤:
- 将
Asciinema提供的JavaScript库封装为Docusaurus支持的React组件.
完成上述实现后, 我们就可以在Markdown文档中引入Asciinema动画文件, 并将其渲染到页面中.
例如:
import AsciinemaPlayer from '@site/src/components/asciinema/react';
<AsciinemaPlayer src="/blog/2024-06-28-demo.cast" />
这样, 最终的渲染效果应该是:
扩展Markdown解析器并实现link语法的渲染
在上一个步骤中, 最终的Markdown文件中需要编写JavaScript代码来完成渲染, 但对于文档编写者来说会面临以下问题:
- 如果文档项目由多人编写, 语法问题的概率会增加.
Markdown迁移难度加大, 因为JS代码只是实现某个功能的一种方式, 而不应该在Markdown文件中显式依赖解决方案空间的细节.
为了解决以上问题, 我们需要优化使用方式, 降低使用者的难度, 同时屏蔽底层的细节.
最终的使用者可能更希望在Markdown中使用link语法来引入Asciinema动画文件, 而在底层我们需要通过Docusaurus
提供的功能进行扩展渲染.
例如, 用户在文档中增加如下内容:
[x](@/assets/blog/2024-06-28-demo.cast)
那么, 最终的渲染效果应该是:
上述实现思路主要是扩展Docusaurus的Markdown解析器, 并对其进行扩展, 最终转换为上一个步骤中的React组件.
在Docusaurus中, 可以通过扩展remark和rehype来实现对Markdown语法解析的扩展.
这种方法允许我们在Markdown解析为AST后进行修改, 从而实现对Markdown的扩展功能.
详细的文档和插件开发信息, 请参考MDX Plugins.
功能实现
封装Asciinema库为React组件
首先, 将Asciinema添加到项目中:
yarn add asciinema-player
接下来, 我们需要封装成React组件.
// import 'asciinema-player/dist/bundle/asciinema-player.css';
import './asciinema-player.css'; // We hacked the CSS of the asciinema-player located at 'asciinema-player/dist/bundle/asciinema-player.css'.
import {FC, useEffect, useRef, useState} from 'react';
import {useColorMode} from '@docusaurus/theme-common';
type Props = {
src: string;
cols: string;
rows: string;
autoPlay: boolean
preload: boolean;
loop: boolean | number;
startAt: number | string;
speed: number;
idleTimeLimit: number;
theme: string;
poster: string;
fit: string;
fontSize: string;
};
const AsciinemaPlayer: FC<Props> = ({src, ...rest}) => {
const [player, setPlayer] = useState<typeof import ('asciinema-player')>()
useEffect(() => {
import("asciinema-player").then(p => {setPlayer(p)})
}, []) // executed once
const { colorMode } = useColorMode();
const ref = useRef<HTMLDivElement>(null);
useEffect(
() => {
const currentRef = ref.current
const instance = player?.create(src, currentRef, {...rest, theme: colorMode === 'dark' ? 'docusaurus-classic-dark' : 'docusaurus-classic-light'});
return () => {
instance?.dispose()
}
}, [src, rest, colorMode, player] // executed every time the array items change
);
return <div ref={ref}/>;
};
export default AsciinemaPlayer;
src/components/asciinema/react/asciinema-player.css
上面的代码中, 我们将Asciinema的JavaScript库封装为React组件.
利用React提供的Ref功能, 将Asciinema操作的DOM组件与React组件关联起来, 以确保React能够集成该组件.
为了避免重复渲染, 需要确保组件在适当的时候被dispose.
为了适配Docusaurus的主题, 我们需要引入自定义的CSS样式. 在Asciinema提供的基础样式上, 添加对Docusaurus主题的支持.
自定义Markdown语法树解析
首先, 我们需要将下面的依赖安装到项目中:
yarn add rehype-katex remark-math
这两个库用于解析Markdown语法树, 并对语法树内容进行修改, 以实现我们的目的.
现在可以开始编写下面的代码来实现
import { visit } from "unist-util-visit";
const plugin = options => {
const transformer = async tree => {
let importInserted = false;
visit(tree, "link", (node, index, parent) => {
if (!node.url.endsWith(".cast")) {
return;
}
if (!importInserted) {
const importNode = {
type: "mdxjsEsm",
value: `import AsciinemaPlayer from '@site/src/components/asciinema/react';`,
data: {
estree: {
type: "Program",
body: [
{
type: "ImportDeclaration",
specifiers: [
{
type: "ImportDefaultSpecifier",
local: { type: "Identifier", name: "AsciinemaPlayer" },
},
],
source: {
type: "Literal",
value: "@site/src/components/asciinema/react",
},
},
],
},
},
};
tree.children.unshift(importNode);
importInserted = true;
}
const jsxNode = {
type: "mdxJsxFlowElement",
name: "AsciinemaPlayer",
attributes: [
{ type: "mdxJsxAttribute", name: "src", value: node.url },
{
type: "mdxJsxAttribute",
name: "theme",
value: "docusaurus-classic-light",
},
{ type: "mdxJsxAttribute", name: "rows", value: 30 },
{ type: "mdxJsxAttribute", name: "idleTimeLimit", value: 3 },
{ type: "mdxJsxAttribute", name: "preload", value: true },
],
children: [],
};
parent.children.splice(index, 1, jsxNode);
});
};
return transformer;
};
export default plugin;
在上述代码中, 我们对link进行了修改, 并将其转换为JSX语法, 这样可以在Markdown中直接使用Asciinema组件.
除了上述代码, 我们还需要在Docusaurus中进行配置以进行功能集成.
参考以下配置来配置Docusaurus:
import rehypeKatex from "rehype-katex";
import asciinema from "./src/components/asciinema/Markdown/Markdown";
export default {
presets: [
[
"@docusaurus/preset-classic",
{
docs: {
path: "docs",
beforeDefaultRemarkPlugins: [asciinema],
rehypePlugins: [rehypeKatex],
},
blog: {
beforeDefaultRemarkPlugins: [asciinema],
rehypePlugins: [rehypeKatex],
},
},
],
],
};
上述配置对Docusaurus中的Docs和Blog进行了配置, 并实现了Markdown中的Asciinema组件的解析.
需要注意的一点是, 我们使用beforeDefaultRemarkPlugins而不是remarkPlugins进行配置的主要原因是,
Docusaurus会对Markdown语法树进行修改, 因此我们需要在其修改之前进行配置, 以确保最终的结果正确.