将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](/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
语法树进行修改, 因此我们需要在其修改之前进行配置, 以确保最终的结果正确.