使用 AsciiDoc 写作

Author Avatar
Equim 2017年12月30日
  • 在其它设备中阅读本文章
Read: 17 minWords: 4,077Last Updated: 18-03-24Written in: AsciiDocLicense: CC-BY-NC-4.0

本文将针对在 Hexo 博客上使用 AsciiDoc 进行写作提出一些建议。

缘起

一开始,我使用 AsciiDoc 只是因为它能输出 manpage。后来,我渐渐地感受到了它的方便之处,而且 Asciidoctor 生成 HTML5 时自带的 CSS 也相当好看。再往后,我也慢慢发现了它相比 Markdown 的种种优势,以及 Markdown 存在的短板。

对我而言,Markdown 最大的问题在于,它过度地原语化了标记语法,追求简洁却过犹不及。当对排版的需求变得越来越复杂时,各式各样的 Markdown 扩展就被衍生出来,却又没有被很好地标准化,导致各个地方的 Markdown 扩展经常不兼容,还经常要在 Markdown 里写 HTML 来“曲线救国”,然而这种硬生生的 HTML 嵌入,不光失去了转换为 HTML 以外的后端的可能性,也失去了它作为“轻量级标记语言”的本意。我身边就有个同学最近在抱怨,自己在本地排版得好好的 Markdown 一发布上去格式就乱了。

当我在 GitHub 上看到越来越多的 README.md 里满是 HTML 标记时,就不禁感叹,我们已经在背离 Markdown 哲学的路上越走越远了。

A Markdown-formatted document should be publishable as-is, as plain text, without looking like it’s been marked up with tags or formatting instructions.

— John Gruber (1973-)
Markdown 语言的缔造者,语出 Markdown philosophy

对于 AsciiDoc 的介绍,基本语法,Markdown 的短板,AsciiDoc 与 Markdown 的对比等话题,这里就不再赘述了,可以自行阅读以下这几篇文章:

在平时编写 AsciiDoc 时,我使用的渲染工具是 Asciidoctor,这是一个由 Ruby 编写的对 Python 原版 asciidoc.py 的一个 drop-in replacement[1],支持 HTML5, docbook, manpage 等后端。

在弄完 Hexo 的 AsciiDoc 渲染插件,并写了一些相应的 CSS 补丁之后,我就将之前几篇排版不佳的文章使用 AsciiDoc 重新排版了一遍,例如双子系统(仮)环境安装指南这篇,里面就涉及要在一个有序列表里插入多个段落和代码块而不打破排版和列表顺序,在 Markdown 下这是很难达成的,但是用 AsciiDoc 就很轻松。

本文也是用 AsciiDoc 编写的,截至本文最后一次被渲染时,所使用的 Asciidoctor 版本为 1.5.6.1[2]

Hexo 渲染器插件

Asciidoctor 有一个 JS 版本 Asciidoctor.js,其原理是基于 Opal 来用 JavaScript 去解释 Ruby[3]

目前已经有了一个基于 Asciidoctor.js 的 Hexo 渲染插件 hexo-renderer-asciidoc,但是并不适合我,原因是

  • 它强行调用了 Hexo 自带的语法高亮器去渲染代码块,但我一直以来都是自己调用 highlightjs,而这个插件也没有提供相关选项。

  • Hexo 标签插件会被莫名其妙地 escape 掉。

  • Asciidoctor 默认自动生成的 ID 对中文不友好。[4]

  • 无法自定义 attribute。

  • 参数的格式写错了,doctype: docbook其实并不会起作用。[5]

于是我决定在原有的基础上 fork 一份,然后直接放在scripts目录下,在 Hexo 渲染时就会自动注册该插件。它的实现也很简单,以下是我的 fork

scripts/index.js
'use strict'

const asciidoc = require('./lib/asciidoc.js')

hexo.extend.renderer.register('ad', 'html', asciidoc, true)
hexo.extend.renderer.register('adoc', 'html', asciidoc, true)
hexo.extend.renderer.register('asciidoc', 'html', asciidoc, true)
scripts/lib/asciidoc.js
'use strict'

const asciidoctor = require('asciidoctor.js')()
const path = require('path')

const fontAwesome = 'https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css'

const opts = { (1)
  safe: 'safe',
  attributes: {
    doctype: 'article',
    showtitle: false,
    icons: 'font', (2)
    idprefix: '',
    idseparator: '-',
    sectids: false
  }
}

function render (data) {
  if (data.path) {
    const entry = path.parse(data.path)
    const assetDir = path.join(entry.dir, entry.name)
    opts.attributes.docdir = assetDir (3)
  }

  return asciidoctor.convert(data.text, opts) +
    '<link href="' + fontAwesome + '" rel="stylesheet">' (2)
}

module.exports = render
1这里是可以和hexo.config合并的,不过暂时先写死了。
2使用了 font-awesome 的图标。
3将 docdir 设置为 Hexo 对应的 asset 目录,这样就能更方便地使用 AsciiDoc 的include语法。

从 Markdown 迁移

[6]

很多例如样式之类的东西因主题而异,下面的建议未必适用于所有人。

语法改写

简单粗暴的例子,概括了大多数常用的语法。

Markdown 版本
## 睡眠
[![header](/res/img/header.jpg)](https://ekyu.moe/)

下面是一个样例,其中 `eq.Sleep` 是 [time.Sleep](https://golang.org/pkg/time/#Sleep) 的超集。<br>
它会 suspend 除当前线程以外所有的 routine,*Equim kernel* 会在之后进入单道程序模式,信号量将大幅度减少。

```go
defer eq.Sleep(5 * time.Hour)
eq.Work()
```

尽管很少有人在意,但 `eq.Sleep` 存在返回值,与 *Equim kernel* 的 sycall **sleep(2)** 相同,如下

<!-- 这段之后得改改 --> (1)

| 返回值 | 意义 |
| - | - |
| 1 | 内核获得了 *happiness* |
| -1 | 内核失去了 *happiness* |
| 0 | 内核得失 *happiness* 的情况未知 |
1Markdown 没有原生的注释语法。
AsciiDoc 版本
== 睡眠
image::/res/img/header.jpg[header,300,200,link=https://ekyu.moe] (1)

下面是一个样例,其中 `eq.Sleep` 是 https://golang.org/pkg/time/#Sleep[time.Sleep] 的超集。 +
它会 suspend 除当前线程以外所有的 routine,_Equim kernel_ 会在之后进入单道程序模式,信号量将大幅度减少。

[source,go]
----
defer eq.Sleep(5 * time.Hour)
eq.Work()
----

尽管很少有人在意,但 `eq.Sleep` 存在返回值,与 _Equim kernel_ 的 sycall *sleep(2)* 相同,如下

// 这段之后得改改

.Return Values of *sleep(2)* (2)
[%header,cols=2*]
|===
|返回值
|意义

|1
|内核获得了 _happiness_

|-1
|内核失去了 _happiness_

|0
|内核得失 _happiness_ 的情况未知
|===
1图片尺寸可控,这是 Markdown 不支持的语法。
2代码块的 label,这是 Markdown 不支持的语法。
Example 1. AsciiDoc 的渲染效果[7]
header

下面是一个样例,其中 eq.Sleeptime.Sleep 的超集。
它会 suspend 除当前线程以外所有的 routine,Equim kernel 会在之后进入单道程序模式,信号量将大幅度减少。

defer eq.Sleep(5 * time.Hour)
eq.Work()

尽管很少有人在意,但 eq.Sleep 存在返回值,与 Equim kernel 的 sycall sleep(2) 相同,如下

Table 1. Return Values of sleep(2)
返回值意义

1

内核获得了 happiness

-1

内核失去了 happiness

0

内核得失 happiness 的情况未知

Hexo 的标签插件

由于标记插件的语法与 AsciiDoc 有所冲突,为了避免不必要的解析错误,我们简单粗暴地采用了“曲线救国”的方案,直接用 passthrough 标记 ++++让 AsciiDoc 解析器对内容不做处理,原原本本地交给下游。

同样地,对于<!-- more -->之类的标记我们也使用这种方法来处理。

扯句题外话,看到这你可能想问,之前不是还说 Markdown 曲线救国不好吗,怎么这回 AsciiDoc 也曲线救国了?如果你仔细观察就会发现,除了下文要提到的删除线标记(这是唯一的例外)以外,所有其他这些“曲线救国”方案针对的都是 Hexo 相关的内容,无论是用 JS 解析 Ruby ,还是标签插件、特殊的 HTML 注释。假如我们抛开 Hexo,完全使用 AsciiDoc 来写作,并直接用 Asciidoctor 来渲染的话,就不需要这些“曲线救国”的东西了。事实上,AsciiDoc + Asciidoctor 本身已经相当好用了。在我的一些个人项目里,前端就直接采用了 Asciidoctor 渲染的页面(毕竟我只是个小后端),我在撰写报告、API 文档、manpage 时也会使用 Asciidoctor,且不论是效率、效果都比 Markdown 要好很多。
Markdown 版本
效果如下图所示

{% asset_img deploy.gif %}
AsciiDoc 版本
效果如下图所示

++++
{% asset_img deploy.gif %}
++++

引用块

由于 Markdown 本身的引用太过简洁,以至于没法写上出处等 metadata,所以 Hexo 也提供了标签插件blockquote。不过,我们用上了 AsciiDoc 之后,就可以直接用它本身的引用语法了。

还是那句话,最终的结果和主题本身的 CSS 关系很大,所以也可能标签插件提供的比 AsciiDoc 原生语法的效果还要好。如果你比较在意,可以自己打些 CSS 的 patch 来修改样式。
Hexo 标签插件版本

{% blockquote "Sound Horizon", Roman(2006) http://shkashi.blog.fc2.com/blog-entry-11.html Sound Horizon 歌詞置き場 %}
君の大好きなこの**旋律**[Melodie(メロディー)]
大空へと響け口**風琴**[Harmonica(アルモニカ)]
天使が抱いた窓枠の**画布**[Toile(トワル)]
ねぇ...その**風景画**[Paysage(ペイサージュ)]
綺麗かしら?
{% endblockquote %}
AsciiDoc 版本
[quote, Sound Horizon, 'Roman(2006), extracted from http://shkashi.blog.fc2.com/blog-entry-11.html[Sound Horizon 歌詞置き場]']
____
君の大好きなこの**旋律**[Melodie(メロディー)] +
大空へと響け口**風琴**[Harmonica(アルモニカ)] +
天使が抱いた窓枠の**画布**[Toile(トワル)] +
ねぇ...その**風景画**[Paysage(ペイサージュ)] +
綺麗かしら?
____
Example 2. Hexo 标签插件的引用块渲染效果

君の大好きなこの旋律[Melodie(メロディー)]
大空へと響け口風琴[Harmonica(アルモニカ)]
天使が抱いた窓枠の画布[Toile(トワル)]
ねぇ…その風景画[Paysage(ペイサージュ)]
綺麗かしら?

Example 3. AsciiDoc 的引用块渲染结果

君の大好きなこの旋律[Melodie(メロディー)]
大空へと響け口風琴[Harmonica(アルモニカ)]
天使が抱いた窓枠の画布[Toile(トワル)]
ねぇ…​その風景画[Paysage(ペイサージュ)]
綺麗かしら?

— Sound Horizon
Roman(2006), extracted from Sound Horizon 歌詞置き場

删除线

Markdown 中有~~strike~~这样的语法可以加删除线,但是 AsciiDoc 却没有,这一点让我很是疑惑。不过,我们倒是可以自己写一个.strike的样式,然后再用 AsciiDoc 的指定 CSS 的语法[8]去调用。

相关细节请参考CSS 补丁一章。

Markdown 版本
删除线标记在 AsciiDoc 中可以用自定义的 CSS 类来实现,~~这也算是“曲线救国”了。~~
AsciiDoc 版本
删除线标记在 AsciiDoc 中可以用自定义的 CSS 类来实现,[.strike]##这也算是“曲线救国”了。## (1)
1原则上,#只需要一个就够了,但实际上并不行,个人猜测这和 Asciidoctor 的 lexer 有关。总之,对于所有可以叠加的标记,如`_*,如果周围是非 ASCII 字符,并且需要标记的段落没有占满一整行的话,建议用叠加版本的。
Example 4. AsciiDoc 的渲染效果

删除线标记在 AsciiDoc 中可以用自定义的 CSS 类来实现,这也算是“曲线救国”了。

对于块级的自定义 CSS,也可以用role来实现。

[role=strike]
* Goal 1
* Goal 2

在 Asciidoctor 中也可以这样简写

[.strike]
* Goal 1
* Goal 2
Example 5. 渲染效果
  • Goal 1

  • Goal 2

一级标题

一个值得注意的细节就是,按照 AsciiDoc 的理念,除非 doctype 是 book,否则一个页面的一级标题只能有一个[9]。原则上说,一级标题确实只应该有一个,也就是整个页面的标题(title)。所以,如果你的页面包含了多个一级标题,渲染器会报错,目录也可能会出现异常。

建议在使用 AsciiDoc 编写 Hexo 博客时完全不使用一级标题。

尚未解决的问题

  • 使用 Asciidoctor 的 HTML5 后端生成的代码比较依赖于 Asciidoctor 为 HTML5 定制的 CSS,这样会造成两个问题。一是我们没有移植所有的 CSS 样式;二是相比倾向于直译到 HTML tag (即偏好 HTML4)而较少利用 CSS 的大多数 Markdown 渲染引擎而言,Asciidoctor 生成的 HTML5 显得较为冗长。

    尽管原版 Asciidoc.py 支持 HTML4 后端[10],但 AsciiDoctor 似乎移除了这个特性[11]
  • AsciiDoc 的代码块 callouts 标记可能会对 highlight.js 造成一些小小的影响[12][13]

CSS 补丁

下面的内容基本都来自 Asciidoctor 自带的默认样式表[14],有少量适应性的更改,不要指望直接照搬就能用。

这段针对 AsciiDoc 的补丁作为我给整个主题(hexo-material)的补丁的一部分,使用了 less 编写,最终收录在了 src/eq-patch.min.css 中。

由于 material 对于文章主体直接使用了 ID (#post-content)而不是类来选择,导致我在写很多选择器的时候经常要带上这个 ID,否则无法覆盖,结果就是 CSS 补丁的体积很大,而且很多地方看上去相当冗余。

还有一些样式出于其复杂程度(例如引入了新字体等)还没有 patch 进去,比如 admonition[15]

现在 admonition 已经有了哦!

——18-01-03

src/_eq-patch.less[16] (节选)
/**
 * 补充的几个样式,也包含一些样式补丁
 */

.strike {
  text-decoration: line-through;
}

/* 由 AsciiDoc 语法 .标题 生成的内容,适用于列表和代码块的标题 */
.title {
  text-rendering: optimizeLegibility;
  text-align: left;
  font-family: 'Noto Serif', 'DejaVu Serif',serif;
  font-size: 1rem;
  font-style: italic;
  line-height: 1.45;
  color: #7a2518;
  font-weight: 400;
  margin-top: 0;
}

/* AsciiDoc 脚注的分割线 */
#footnotes hr {
  width: 20%;
  min-width: 6.25em;
  margin: 8em 0 0.75em 0; /* 有修改 */
  border-width: 1px 0 0 0;
}

/* AsciiDoc 的 admonitionblock */
#post-content {
  .admonitionblock {
    & > table {
      border: 0;
      background: none;
      width: 100%;
    }
    table td {
      border: 0; /* 这一句是补充的,原样式给 td 设置了 border */

      &.icon {
        text-align: center;
        width: 80px;

        img {
          max-width:none;
        }
        .title{
          font-weight: bold;
          font-family: 'Open Sans', 'DejaVu Sans', sans-serif;
          text-transform: uppercase;
        }
      }

      &.content {
        padding-left: 1.125em;
        padding-right: 1.25em;
        border-left: 1px solid #ddddd8;
        color: rgba(0,0,0,.6);

        &>:last-child>:last-child {
          margin-bottom:0;
        }
      }
    }

    td.icon {
      [class^="fa icon-"] {
        font-size: 2.5em;
        text-shadow: 1px 1px 2px rgba(0,0,0,.5);
        cursor: default;
      }
      .icon-note:before {
        content: "\f05a";
        color:#19407c;
      }
      .icon-tip:before {
        content: "\f0eb";
        text-shadow: 1px 1px 2px rgba(155,155,0,.8);
        color: #111;
      }
      .icon-warning:before {
        content: "\f071";
        color: #bf6900;
      }
      .icon-caution:before {
        content: "\f06d";
        color: #bf3400;
      }
      .icon-important:before {
        content: "\f06a";
        color: #bf0000;
      }
    }
  }
}

/* AsciiDoc 的 Q&A */
.qanda > ol > li > p > em:only-child {
  color: #1d4b8f;
}

/* AsciiDoc 的 blockquote */
.quoteblock {
  border-left: 4px solid #ddd; /* 这一句来自 hexo-material 原有的样式,为了能让左边的 border 连起来 */

  #post-content & blockquote {
    border-left: 0px; /* 既然 border 已经在 .quoteblock 里直接做了,这里就不需要了 */
  }

  .attribution {
    margin-top: 0.5em;
    margin-right: 0.5ex;
    text-align: right;
  }
}

.quoteblock .attribution, .verseblock .attribution {
  font-size: .9375em;
  line-height: 1.45;
  font-style: italic;

  cite {
    display: block;
    letter-spacing: -0.05em;
    color: rgba(0,0,0,0.6);
  }
}

/* AsciiDoc 的 example block */
.exampleblock > .content {
  border-style: solid;
  border-width: 1px;
  margin-bottom: 1.25em;
  padding: 1.25em;
  -webkit-border-radius: 4px;
  border-radius:4px;
  background-color: #fffef7;
  border-color: #e0e0dc;
  box-shadow: 0 1px 4px #e0e0dc;
  -webkit-box-shadow: 0 1px 4px #e0e0dc;
}

/* AsciiDoc 的 sidebar block */
.sidebarblock {
  border-style: solid;
  border-width: 1px;
  border-color: #e0e0dc;
  margin-bottom: 1.25em;
  padding: 1.25em;
  background: #f8f8f7;
  border-radius:4px;
  -webkit-border-radius: 4px;

  & > .content > .title {
    font-size: 1.6875em;
    color: #7a2518;
    margin-top: 0;
    text-align: center;
    font-style: normal;
  }
}

/* Asciidoc 的代码块 callouts */
.conum[data-value] {
  display: inline-block;
  color: #fff !important;
  background-color: rgba(0,0,0,.8);
  -webkit-border-radius: 100px;
  border-radius: 100px;
  text-align: center;
  font-size: .75em;
  width: 1.67em;
  height: 1.67em;
  line-height: 1.67em;
  font-family: 'Open Sans', 'DejaVu Sans', sans-serif;
  font-style: normal;
  font-weight: bold;

  * {
    color: #fff !important;
  }
  &+b {
    display: none;
  }
  &:after {
    content: attr(data-value);
  }
  pre {
    position: relative;
    top: -.125em;
  }
}
b.conum * {
  color: inherit !important;
}
.conum:not([data-value]):empty {
  display: none;
}

/* 对于所有 Asciidoctor 里用 table 做 list 的 hack 的补丁 */
#post-content {
  .literalblock+.colist > table,
  .listingblock+.colist > table { /* 这个选择器有修改 */
    margin-top: -.5em;
  }
  .hdlist > table, .colist > table {
    border: 0;
    background: none;
  }
  .hdlist > table > tbody > tr, .colist > table > tbody > tr {
    background: none;
  }
  .hdlist td, .colist td {
    border: 0; /* 这一句是补充的,原样式给 td 设置了 border */
  }
  .colist td:not([class]):first-child {
    padding: .4em .75em 0 .75em;
    line-height: 1;
    vertical-align: top;
  }
  .colist td:not([class]):first-child img {
    max-width: none;
  }
  .colist td:not([class]):last-child {
    padding: .25em 0;
  }
}

1. 中文确实不知道该怎么说,它的意思是“可以直接地替代另一个东西而不用做任何的修改,就像原来在用另一个东西的时候一样”。
2. 来自asciidoctor-version这个变量
3. 用一门脚本语言去解释另一门脚本语言,可以说是很有“曲线救国”的味道。
5. Asciidoctor.js 和原版 Asciidoctor 的 API 是一样的,可以参考 Ruby API OptionsAsciidoctor.js User Manual。正确的做法是把doctype放在子对象attributes的下面,尽管确实没有必要设置doctype
6. 讲道理,为了写这章,我要在 AsciiDoc 里同时 escape 掉 AsciiDoc 自己、Hexo 标签插件的 escape 还有它们的 escape 的 escape 还真有点挑战性。
7. 在这个样例里,二级标题被略去了,因为如果保留的话会对本文目录的生成造成影响。
15. 参考 Admonition
16. 我只是个小后端,前端代码写的不好请见谅。另外重度怀疑如果哪天更新了主题这些 CSS 又得重新搞…

知识共享许可协议
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。

本文链接:https://ekyu.moe/article/write-in-asciidoc/