Macrohard Markdown 博客/幻灯片生成工具介绍

概述

本文介绍 Macrohard 站点使用的 Markdown 博客幻灯片生成工具,需要读者具备 HTML 和 Markdown 基础。该实现基于 Markdown-It 解析器以及其强大的衍生插件,部分缺少对应插件或现有插件无法满足需求的的特性则由我自行实现,这些特性的实现将在完善后作为插件开源。

本扩展采用纯前端实现,因此发布内容不需要本地编译,与同类产品 Quarto 不同(基于 Pandoc)。由于个人时间和精力有限,故仅实现了 R Markdown 扩展语法的一个子集。

预览

R Markdown 预览可使用 Quarto 提供的插件,详见 Quarto 官网VSCode Quarto 插件

基本格式规范

按照 R Markdown 规范,每一篇 Markdown 文章开始都应具有如下形式的 YAML 元数据,元数据具有如下所示的形式:

---
title: '`std::cout << "Hello, world!" << std::endl;`'
---

每篇 R Markdown 文章都必须定义 title,即文章的标题。上面的文章标题在处理后将形成像下面这样的 Markdown 一级标题:

# `std::cout << "Hello, world!" << std::endl;`

有关元数据的相关规范参见 元数据,不在本节讨论范围。

使用者应该严格区分文章标题章节标题。 需要指出,每一篇文章有且仅有一个文章标题,文章标题通过一级标题(如:# 文章标题)进行定义。 由于一级标题的作用是定义整篇文章的标题,而在 R Markdown 中,一级标题已经通过元数据的方式定义了(渲染框架处理后会在文章内容中嵌入这样一个唯一的一级标题),因此,文章中不应该出现任何一级标题的定义。 文章的章节标题通过 ##### …… 进行定义,最多达到 ######,分别代表一级章节标题、二级章节标题、……、五级章节标题。

基本扩展

扩展 语法 效果
上标 Foo^bar^ Foobar
下标 Foo~bar~ Foobar
下划线 _Foo_ Foo 1
删除线 ~~Foo~~ Foo

属性语法

通过在 Markdown 语句块后附加 { #id .class attribute="value" } 可以为该语句块指定属性。例如,下面的 Markdown 语句:

[Foo]{ #Foo-Id .Foo-Inline FooProperty="Foo-Property" }

将会产生如下的 HTML 标记:

<span id="Foo-Id" class="Foo-Inline" FooProperty="Foo-Property">Foo</span>

对于像标题这些特殊的场景,则可以按照下面的方式添加属性:

## Foo{ .class="Foo-Heading" }

这将产生如下的 HTML 标记 2

<h2 id="Foo" class="Foo-Heading">Foo</h2>

一般而言,仅建议使用该语法来定义 idclass 等常用属性。

要指定复杂属性,如样式属性 style,可以使用下面的语法:

## Bar {data-style-display="block" data-style-text-align="center"}

这将产生如下的 HTML 标记:

<h2 id="hello" style="display: block; text-align: center;">Bar</h2>

data- 开头的属性是本扩展特有的属性前缀,表示该属性需要经过解析器的特殊处理后才能生效。在后面还会介绍更多类似的属性前缀。

下面的常用的样式属性已经被预定义为简化形式:

  • width:与 data-style-width 等价。
  • height: 与 data-style-height 等价。
  • color:与 data-style-color 等价。

此特性还有一种常见的使用场景,比如下面的这个例子就可以实现在新页面打开链接:

[Macrohard](https://www.macrohard.fun){target="_blank"}

解析器中预定义了 left(左对齐)、center(居中)、right(右对齐)、justify(分散对齐),可以通过下面的代码实现混合对齐效果:

> 逝者如斯夫,不舍昼夜。
>
> ——《论语》{.right}

逝者如斯夫,不舍昼夜。

——《论语》

对齐属性是针对块级元素而言的,因此对内联元素(比如图片、内联代码等)使用该属性不会产生预期的效果。对块级元素使用该属性时,其内部的内容会相应地进行对齐。上面的例子中 ——《论语》{.right} 看起来像是对内联元素使用了对齐属性,但实际上,这行文本在解析后会被放置在块级元素 <p> 元素内,进而实现其内部文本右对齐的效果。

容器

使用 ::: ::: 可以定义一个容器(默认为 <div>),结合上面提到的属性语法,可以为容器指定相应的属性。容器允许嵌套,嵌套时,定义外层容器的冒号数量(至少 3 个)需要大于内层。

下面的代码定义了一个栏目块(class="columns"),其中包含了三栏文本(class="column")。

:::: { #custom-block .columns width="200" }
::: { .column }
### 第一栏

第一栏

第一栏
:::
::: { .column }
### 第二栏

第二栏

第二栏
:::
::: { .column }
### 第三栏

第三栏

第三栏
:::
::::

columnscolumn 均是本 Markdown 扩展中预定义的 CSS 类,用于实现分栏效果。因此,上面的代码可以实现如下的效果(注意,不允许在幻灯片的 column 内定义标题):

第一栏

第一栏

第一栏

第二栏

第二栏

第二栏

第三栏

第三栏

第三栏

除了默认的 <div> 容器外,还可以通过 is 属性指定使用其他类型的容器,例如:

:::{ .right data-is="figure" }
![](./doro-maodie.gif)
:::

这会产生如下效果:

当图片没有标题时,图片将被 直接 渲染为 <img> 元素。可以使用 ::: { data-is="figure" } ::: 来定义一个图片容器,并为容器指定一些属性来实现特定的排版效果。 当为图片指定标题时,图片本身会被渲染为 <img> 元素,标题会被渲染为 <figcaption> 元素,二者会被包含在一个 自动创建<figure> 元素内,指定的属性将应用于该 <figure> 元素,此时不应该再对图片使用 ::: { data-is="figure" } :::

例如,直接对无标题的图片添加居中属性将不会产生任何效果,因为 <img> 元素是行内元素,行内元素无法被居中对齐:

![](doro-maodie.gif){.center}

会产生如下 HTML 标记:

<img src="doro-maodie.gif" class="center" data-viewable />

为图片添加标题后,图片和标题会被包含在一个 <figure> 元素内,此时对图片使用居中属性将会生效:

![Doro 💖 耄耋](doro-maodie.gif){.center}

会产生如下 HTML 标记:

<figure class="center">
  <img src="doro-maodie.gif" data-viewable />
  <figcaption>Doro 💖 耄耋</figcaption>
</figure>
Doro 💖 耄耋
Doro 💖 耄耋

你可能注意到了 data-viewable 属性,该属性用于指示图片是可查看的(viewable),即点击图片后会弹出查看器 3 ,在查看器中可以进行拖拽、旋转、镜像、放大、缩小等操作。

代码高亮

内联代码

可以通过为内联代码块附加属性的方式来高亮内联代码块,例如:

`p { color: red; }`{ .language-css }

效果如下:

p { color: red; }

Mix inline code with ordinary text

若要在内联代码块中使用反引号 `,则需要像下面这样使用至少两个反引号作为限定符,并且在反引号与限定符相邻的情况下需要在反引号与限定符之间添加空白字符:

`` ` ``

若内联代码块中的反引号连续出现的最大次数为 $N$ ,则外层需要至少 $N + 1$ 个反引号作为限定符,例如:

```` ``` ````

这将产生:```

代码块

除了基本的高亮,还可以通过附加属性 code-line-numbers 来指定需要强调的行号,例如:

```c { code-line-numbers="1-2,5,7" }
#include <stdio.h>
int a = 0;
int b = 1;
foo();
bar();
/*---------------*/
assert(a != b);
/*===============*/
#ifdef _DEBUG
printf("hello, world!");
#endif
```

可以实现如下效果:

#include <stdio.h>
int a = 0;
int b = 1;
foo();
bar();
/*---------------*/
assert(a != b);
/*===============*/
#ifdef _DEBUG
printf("hello, world!");
#endif

若设置 code-line-numbers="true",则对所有行应用强调效果。

若要在代码块中使用 ```,则代码块限定符中的反引号 至少 需要 4 个,以此类推,若代码块中反引号连续出现次数最大值为 $N$ ,则代码块限定符中的反引号至少需要 $N + 1$ 个。

标注块

标注块是对 Markdown 引用块的增强,支持更丰富的标注类型,结合使用容器与属性语法 ::: { .callout-type } ::: 可以定义一个标注块,其中 callout-type 可以是以下类型之一:

callout-note

::: {.callout-note}
笔记。
:::

笔记。

callout-tip

::: {.callout-tip}
提示。
:::

提示。

callout-important

::: {.callout-important}
重点。
:::

重点。

callout-warning

::: {.callout-warning}
警告。
:::

警告。

callout-caution

::: {.callout-caution}
注意。
:::

注意。

公式

本扩展支持使用 $...$推荐)及 \(...\) 作为行内公式定界符,$$...$$推荐)及 \[...\] 作为块级公式定界符。

theorem (EN, left aligned) 定理(中文,居中) formula (right-aligned)
Newton’s Law II 牛顿第二定律 $\boldsymbol{F} = m\boldsymbol{a}$
Law of Universal Gravitation 万有引力定律 $\boldsymbol{F} = G\frac{Mm}{\|r\|^3}\boldsymbol{r}$
$$\boldsymbol{e}_r = \frac{\boldsymbol{r}}{\|\boldsymbol{r}\|}.$$
$$\begin{equation} y = \boldsymbol{w}^\mathsf{T}\boldsymbol{x} + b y = ax+b \end{equation}$$

传送

本扩展支持传送(Teleport)功能,即将一个元素的内容传送到另一个位置进行显示。使用传送功能时,需要通过指定 data-teleport-to 属性指定传送的目标容器,文档加载后,传送源会作为整体被移动到传送目标容器内进行显示。

例如,标准 Markdown 语法无法在表格内嵌入复杂元素(比如列表、代码块等),但通过传送功能可以实现这一点。下面的代码展示了如何使用传送功能将复杂元素放置在表格中:

:::{data-teleport-to="strong-ordering"}
- `a == b` $\Rightarrow$ `f(a) == f(b)`。
- 关系 $a < b$、$a = b$、$a > b$ 有且仅有一个成立。
:::

:::{data-teleport-to="weak-ordering"}
- `a == b` $\nRightarrow$ `f(a) == f(b)`。
- 关系 $a < b$、$a = b$、$a > b$ 有且仅有一个成立。
:::

:::{data-teleport-to="partial-ordering"}
- `a == b` $\nRightarrow$ `f(a) == f(b)`。
- 关系 $a < b$、$a = b$、$a > b$ 可能都不成立。
:::

|顺序|定义{.center}|<!-- 这里使用 .center,定义一栏仅居中标题,列表保持靠左 -->
|:-:|---|
|`std::strong_ordering`|{#strong-ordering}|
|`std::weak_ordering`|{#weak-ordering}|
|`std::partial_ordering`|{#partial-ordering}|

这会产生如下效果:

  • a == b $\Rightarrow$ f(a) == f(b)
  • 关系 $a < b$ $a = b$ $a > b$ 有且仅有一个成立。
  • a == b $\nRightarrow$ f(a) == f(b)
  • 关系 $a < b$ $a = b$ $a > b$ 有且仅有一个成立。
  • a == b $\nRightarrow$ f(a) == f(b)
  • 关系 $a < b$ $a = b$ $a > b$ 可能都不成立。
顺序 定义
std::strong_ordering
std::weak_ordering
std::partial_ordering

有了传送功能,可以方便地将复杂的元素放置在文档的其他位置,从而实现更灵活的布局,这在创建表格时非常有用。

流程图

通过下面的方式可创建 mermaid 流程图:

<!-- 注意大括号,这里需要用到属性语法 -->
```{mermaid .center width="50%"}
graph TD
    A[开始] --> B{是否满足条件?}
    B -- 是 --> C[执行操作]
    B -- 否 --> D[结束]
    C --> D
```

以这样的方式创建的流程图会在原地产生一个 <img> 元素。

graph TD
    A[开始] --> B{是否满足条件?}
    B -- 是 --> C[执行操作]
    B -- 否 --> D[结束]
    C --> D

以上面所述的方式创建的流程图是无法显示标题的,并且在尺寸调节上存在一定问题。要解决这些问题,需要借助于一个图片 URL 置空的 <img> 元素,并在定义流程图时指定 data-override-image 属性为插图元素的 id(通过属性语法定义)。这样,流程图创建后,会覆盖插图元素:

```{mermaid .center data-override-image="flowchart"}
graph TD
    A[开始] --> B{是否满足条件?}
    B -- 是 --> C[执行操作]
    B -- 否 --> D[结束]
    C --> D
```

![$ABCD$ `flowchart`](){#flowchart .center width="50%"}
graph TD
    A[开始] --> B{是否满足条件?}
    B -- 是 --> C[执行操作]
    B -- 否 --> D[结束]
    C --> D
$ABCD$ flowchart
$ABCD$ flowchart

复杂表格

普通的 Markdown 表格语法存在如下局限性:

  • 无法在表格内嵌入复杂元素(可以通过传送语法绕过此限制);
  • 仅支持水平表格(记录按行显示),无法创建垂直表格(记录按列显示)以及复合表格(记录按行和列混合显示)。
  • 无法实现复杂的单元格合并效果。

通过使用容器语法结合属性语法,可以创建复杂的表格。例如,下面的代码创建了一个垂直表格:

::::{data-as="table" data-type="grid-vertical"}
:::{data-type="properties"}
- 属性 A{color="red"}
- 属性 B{color="green"}
- 属性 C{color="blue"}
:::
:::{data-type="properties"}
- 属性 1{color="#123456"}
- 属性 2{color="#789abc"}
- 属性 3{color="#def012"}
- 属性 4{color="#345678"}
:::
:::{data-type="record"}
- A1
- B1
- C1
:::
:::{data-type="record"}
- A2
- B2
- C2
:::
:::{data-type="record"}
- A3
- B3
- C3
:::
:::{data-type="record"}
- A4
- B4
- C4
:::
:::{data-type="caption"}
`grid-vertical` 表格
:::
::::
  • 属性 A
  • 属性 B
  • 属性 C
  • 属性 1
  • 属性 2
  • 属性 3
  • 属性 4
  • A1
  • B1
  • C1
  • A2
  • B2
  • C2
  • A3
  • B3
  • C3
  • A4
  • B4
  • C4

grid-vertical 表格

外层容器字段说明:

  • data-as="table":指示该容器应被渲染为表格。
  • data-type:表格类型,可选值包括:
    • plain-horizontal: 不包含属性标题的普通表格。
    • plain-vertical: 不包含属性标题的普通垂直表格。
    • horizontal: 包含属性标题的水平表格。
    • vertical: 包含属性标题的垂直表格。
    • grid-horizontal: 包含属性标题的复合水平表格。
    • grid-vertical: 包含属性标题的复合垂直表格。

内层容器字段说明:

  • data-type
    • properties:用于标识表格属性字段:
      • 当表格类型为 plain-horizontalplain-vertical 时,忽略该容器;
      • 当表格类型为 horizontal 时,表示属性标题行;
      • 当表格类型为 vertical 表示属性标题列;
      • 当表格类型为 grid-horizontal 时,第一个 properties 容器表示属性标题行,第二个 properties 容器表示属性标题列;
      • 当表格类型为 grid-vertical 时,第一个 properties 容器表示属性标题列,第二个 properties 容器表示属性标题行。
    • record:表格的记录字段, 每个 record 容器表示表格中的一行或一列,具体取决于表格类型:
      • 当表格类型为 plain-horizontalhorizontal 时,一条记录代表表示表格中的一行;
      • 当表格类型为 plain-verticalvertical 时,一条记录表示表格中的一列;
      • 当表格类型为 grid-horizontal 时,一条记录表示表格中的一行;
      • 当表格类型为 grid-vertical 时,一条记录表示表格中的一列。
    • caption:表格标题容器,可选,表示表格的标题。可以通过属性语法为标题容器指定属性,例如 data-style-caption-side="top"data-style-caption-side="bottom" 来指定标题显示在表格的上方或下方,默认情况下,标题显示在表格的下方。

从本质上讲,上述语法是对容器按行或列进行展开。

定义在容器中的所有元素最终会被转换为 <th><td> 元素,具体取决于其在表格中的位置。另外,可通过属性语法定义 rowspancolspan 属性来实现复杂的单元格合并效果。

图片增强

公式、代码块和文本可以向下面这样在图片标题中混合使用:

![普通文本 `code` $f\left(x\right)$](./doro-maodie.gif){ .center }
普通文本 code $f\left(x\right)$
普通文本 code $f\left(x\right)$

另外,点击图片后可以使用 ImageViewer 工具查看图片,该工具支持图片放大、缩小、旋转等功能。

注音

可以使用下面的语法为文字添加注音:

[B1][B2][B3]...[BN]^(T1)(T2)(T3)...(TN)

其中,B1B2、……、BN 分别代表需要添加注音的文字块,T1T2、……、TN 分别代表对应文字块的注音内容。注音内容可以是日语假名、汉语拼音等。上述语句会被转换为如下等价的 HTML 标记:

<ruby>
    <rb>B1</rb><rt>T1</rt>
    <rb>B2</rb><rt>T2</rt>
    ...
    <rb>BN</rb><rt>TN</rt>
</ruby>

例如:

- [明日]^(あした)
- [明][日]^(míng)(rì)
- [郵][便][局]^(ゆう)(びん)(きょく)

会产生如下效果:

  • 明日あした
  • míng
  • ゆう便びんきょく

在将一个词拆分注音时,注音的数量必须与文字块的数量一致,否则该注音将不会被渲染。鼠标光标悬停在注音文字上方时,会弹窗显示放大的注音效果以提升可读性。在拆分注音的情况下,可以点击弹窗中的按钮切换文字块单独注音和词语整体注音两种显示模式。

定义脚注

这是一个脚注示例 [^FootnoteExample]。

[^FootnoteExample]: 这是脚注的内容。

这是一个脚注示例 4

当鼠标悬停在脚注链接上时,将会显示脚注的内容。默认情况下,脚注内容将放置在页面底端。若要自定义脚注内容显示的位置,可以在文档元信息中填写 footnote-container: footnotes 并创建一个容器,将其 id 设置为 footnotes 即可自动生成脚注列表(footnotes 根据需要进行修改)。

footnote-container: footnotes
## 脚注
::: {#footnotes}
:::

HTML

为防止 XSS 攻击,本扩展默认不渲染 HTML 标记。但有时我们确实需要在文档中嵌入 HTML 代码,这时可以使用下面的语法:

```{data-html}
<iframe src="presentation.html" width="100%" height=800px"></iframe>
```

这会产生如下效果:

<iframe src="presentation.html" width="100%" height=800px"></iframe>

定义引用

要定义引用,需要提供一个 BibTeX 文件(例如 references.bib),并在文档元信息中指定 bibliography: references.bib。引用的语法为 [@citation-key],其中 citation-key 是 BibTeX 中定义的引用键。例如:

[@shao2024explore; @fu2024featup] 提出了 xxx 方法

[@zhou2022extract] 提出了 xxx 方法

[@shao2024explore]  探索了 xxx

产生如下引用效果:

[1], [2] 提出了 xxx 方法

[3] 提出了 xxx 方法

[1] 探索了 xxx

当鼠标悬停在引用链接上时,将会显示引用的详细信息。默认情况下,不会为引用生成单独的章节,若要列出引用的内容,可以在文档元信息中填写 reference-container: references 并创建一个容器,将其 id 设置为 references 即可自动生成参考文献列表(references 根据需要进行修改):

reference-container: references
## 参考文献

::: {#references}
<!-- 参考文献在此处生成 -->
:::

参考文献

脚注

  1. 该特性与 R Markdown 的行为不同,R Markdown 中仅支持使用 [Foo]{.underline} 来实现下划线。在标准 Markdown 语法中 _Foo_ 将会产生斜体的 Foo,而 *Foo* 已经具有一样的功能,故这里决定覆盖标准 Markdown 的行为,提供一种更加简便的产生下划线的扩展语法。

  2. 在不显式为标题指定 id 属性的情况下,本扩展将为标题自动提供 id 属性,并生成锚点元素,方便在页内跳转。

  3. 该查看器基于 fengyuanchen/viewerjs 实现。

  4. 这是脚注的内容。