多么炫酷的工具、排版,都无法掩盖我的写作能力低下🤣。

引言 Link to heading

文档排版一直是老大难问题,我最开始就是使用 Word 一把梭。通过修改一套合适的 .dotm 格式模板,应付了大学第一年的文档写作。

然而,这种方式暴露出越来越多的问题:首先,word 格式实在太灵活了,一个不经意的操作就可能导致排版混乱且无法恢复;其次,随着我将主力设备切换到 MacOS,Word 的兼容性问题也逐渐显现。比如 MacOS 默认会以兼容模式打开 docx 文件,而且不支持修改底层样式和文档字体。

后面我就接触到了 LaTeX。不得不说,LaTeX 治好了我的排版强迫症,但是无限延长了写作时间。我还记得我第一次用 LaTex 写《中级微观经济学》课程作业,踩坑、查文档、调格式……一个手写只需要 15 分钟的作业硬是花了我一晚上。当然,我也不是没有收获,起码现在用 LaTex 语法写公式还算得心应手。如果你还在痴迷使用 Latex 记笔记,尤其是需要写大量公式的理科生,可以参考一下:

How I’m able to take notes in mathematics lectures using LaTeX and Vim

这篇文章把 LaTeX + Vim 的工作流发挥到了极致,也是促使我学习 LaTeX 和 Vim 的原因。

当然,LaTeX 现在也被我放弃了。起码在写作阶段,使用 LaTeX 并不是一件非常美妙的事情。作为双语用户,我需要时不时在中文输入法和英文键盘之间切换。LaTeX 大量的语法细节让我额外在中、英文间反复跳跃,实在不是一件愉快的事情。

Markdown 是写作的救星 Link to heading

事情的转机出现在我安装了一台黑苹果系统后。疫情期间实在无聊,我把自己的联想 Thinkpad T480s 倒腾上了黑苹果系统。Ulysses、MWeb、Typora 等等 Markdown 编辑器开始映入眼帘,随后三年时间我都使用 Typora 作为主力写作工具。Typora 宣布收费时我就第一时间购买了三台授权,至今还在使用。

Markdown 语法简单、易读、易写,而且 Typora 的所见即所得编辑器让我可以专注于写作。Typora 主题可以快速调整到一个比较优美的样式。课程作业、报告、博客等等用 Markdown 来完成远比 Word 和 LaTeX 强。

但是 Typora 并不是完美的,一个不要求排版的课程报告还好说,对排版要求比较细致的老师,Typora 的主题定制就有点力不从心了。Typora 的主题定制还是基于 CSS,所以很多 Word 上的排版效果是无法实现的。例如图注、表格标题、页眉页脚等等。

用 Pandoc 实现 Markdown 到 Word 的转换 Link to heading

所幸,Typora 的导出选项中有 Word 格式,我也发现了 Pandoc 这个神器。

Typora 就是基于 Pandoc 导出的,我摸索出了一条使用 Pandoc 将 Markdown 转换为 Word 的路径,支持所有课程作业需要的板式:图注、脚注、页眉页脚。这几周随着我对 Pandoc 研究的深入,我也成功实现了参考文献、摘要、关键词等自动化生成。可以说实现了一套高度可用的 Markdown 到 Word 的转换工作流。

这套工作流不仅可以转化成 Word 或 PDF 这种适合提交作业的格式,也可以快速生成博文。一次写作,双重享受🐶。

方法简介 Link to heading

因为时间有限,我这里大概列出我做了什么:

1. 学习 Pandoc 的基本用法 Link to heading

pandoc 文档 几乎提供了所有关于 cli 如何操作的知识。大致了解其工作原理对后面使用 pandoc 排版很有帮助。建议粗略看一下这是啥玩意儿,然后上手试试格式转换过程。

2. 生成一套自己的样式参考文档 Link to heading

1pandoc -o custom-reference.docx --print-default-data-file reference.docx

这条命令生成一个 custom-reference.docx 文件,修改其中的样式文件就可以做到调整样式。你可以参考学校的论文排版样式要求来修改,注意只能对已经有定义的样式进行修改。

大部分样式都基于一个叫做 “正文” 的样式。你可以首先修改 “正文” 的字体、大小、行间距等。

“First Paragraph”和“正文文本”定义了文档正文的样式,“Heading 1”…“Heading n”用来调整各级标题的样式,你可以在「修改样式」 -> 「编号」 上定义一个多级编号。

页眉、页脚也可以提前定义,比如我就定义了三段式的页眉,通过「插入」 -> 「域」的方式插入页码、标题等。

你也可以试试我的:reference.docx

3. 使用自定义 filter Link to heading

简单使用样式文档已经可以满足绝大部分需求了,但是我还想插入摘要、关键词,对编号样式进行细致修改等等,这就需要使用 Pandoc 的 filter 功能了。

pandocker-lua-filters

这里定义了很多有用的 filter,比如docx-extract-bullet-lists可以修改默认的无序列表样式。我简单写了一个支持插入摘要、关键词的脚本,在参考文件中定义 Abstract 和 Keywords 两种样式之后就可以自动生成:

 1if FORMAT == "docx" then
 2  local function abstract_to_divs(doc)
 3      if #doc.meta.keywords then
 4          local keywords_text = pandoc.utils.stringify(doc.meta.keywords)
 5          local keywords_para = pandoc.Para(pandoc.Str(keywords_text))
 6          local keywords_div = pandoc.Div(keywords_para)
 7          keywords_div.attr.attributes["custom-style"] = "Keywords"
 8
 9          table.insert(doc.blocks, 1, keywords_div)
10      end
11
12      if #doc.meta.abstract then
13          local abstract_text = pandoc.utils.stringify(doc.meta.abstract)
14          local abstract_para = pandoc.Para(pandoc.Str(abstract_text))
15          local abstract_div = pandoc.Div(abstract_para)
16          abstract_div.attr.attributes["custom-style"] = "Abstract"
17
18          table.insert(doc.blocks, 1, abstract_div)
19          doc.meta.abstract = nil
20      end
21
22      return doc
23  end
24
25  return { { Pandoc = abstract_to_divs } }
26end

4. 使用 citeproc + zotxt + biblatex 生成参考文献 Link to heading

这一步网上已经有教程了,我觉得这篇教程写的最好:

使用Markdown搭配Pandoc撰写学术论文的详细指南 - 知乎

不过我建议使用下面的 csl 文件,你可以运行 pandoc --version 找到默认配置目录,重命名你下载的 csl 文件为 default.csl 就可以了。

redleafnew/Chinese-STD-GB-T-7714-related-csl: GB/T 7714相关的csl以及Zotero使用技巧及教程。

5. 使用 pandoc-crossref Link to heading

这篇教程的「交叉引用」部分写的很好:

Pandoc 学术指南

6. 用脚本来实现转换 Link to heading

每次都运行一长串的 pandoc … 命令太麻烦了,我写了两个脚本分别简化 markdown to word 和移动图片并按照 Markdown 扩展语法复制两个脚本:

 1function md2word() {
 2    if [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "help" ]; then
 3        echo "Usage: md2word <file> [pandoc options]"
 4        return 1
 5    fi
 6
 7    local file="$1"
 8    shift
 9
10    osascript ~/bin/closedoc.scpt "${file%.md}.docx" # 这个是用来关闭已经打开的 word 文件的
11
12    /opt/homebrew/bin/pandoc \
13        -s \
14        --filter pandoc-crossref \
15        -L docx-table-custom-style.lua \
16        -L pandoc-zotxt.lua \
17        -L 5.4/pandocker/docx-extract-bullet-lists.lua \
18        -L 5.4/pandocker/docx-abstract-keywords.lua \
19        --from markdown+east_asian_line_breaks \
20        "$@" \
21        -M link-citations=false \
22        --shift-heading-level-by -1 \
23        --citeproc \
24        -M titleDelim='' \
25        -M figureTitle='图' -M tableTitle='表' \
26        -M figPrefix='图' -M eqnPrefix='公式' -M tblPrefix='表' -M secPrefix='' \
27        "$file" -o "${file%.md}.docx"
28
29    open "${file%.md}.docx"
30}
31
32function img() {
33    if [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "help" ]; then
34        echo "Usage: img <filename>"
35        return 1
36    fi
37
38    # make sure image.png | image.jpg | image.jpeg | image.gif | image.bmp | image.tiff at least one exists
39    local file
40    local ext
41    for ext in png jpg jpeg gif bmp tiff; do
42        file="image.$ext"
43        if [ -f "$file" ]; then
44            break
45        fi
46    done
47
48    if [ ! -f "$file" ]; then
49        file=$(find . -maxdepth 1 -type f \( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.gif" -o -iname "*.bmp" -o -iname "*.tiff" \) | fzf)
50        ext=$(echo "$file" | awk -F. '{print tolower($NF)}')
51    fi
52
53    if [ ! -f "$file" ]; then
54        echo "Error: No image file found"
55        return 1
56    fi
57
58    # replace '-' with ' ' and make it title
59    local title=$(echo "$1" | sed 's/-/ /g' | awk '{print toupper(substr($0,1,1))substr($0,2)}')
60
61    mv "$file" "./assets/$1.$ext"
62    echo "![$title](./assets/$1.$ext){#fig:$1 height=300px}" | pbcopy
63    echo "![$title](./assets/$1.$ext){#fig:$1 height=300px} Copied to clipboard"
64}

里面还包括一个 Apple OS Script 用来自动关闭已经打开的 Word 文件:

 1on run (args)
 2    tell application "Microsoft Word"
 3        if (count args) = 0 then
 4            return
 5        end if
 6        
 7        set targetDocumentName to item 1 of args -- 设置要检查的文档名
 8        set documentFound to false -- 标记是否找到文档
 9        
10        -- 通过索引遍历所有打开的文档
11        repeat with i from 1 to count documents
12            set theDoc to document i -- 通过索引获取文档
13            set thisName to name of theDoc as string -- 获取文档名称
14            set cleanDocumentName to my trimText(thisName) -- 清理文档名称
15            
16            if cleanDocumentName is targetDocumentName then
17                set documentFound to true -- 标记找到了文档
18                -- 检查文档是否已保存
19                if saved of theDoc is false then
20                    display dialog "The document '" & targetDocumentName & "' has unsaved changes. Close without saving?" buttons {"Yes", "No"} default button 2
21                    if the button returned of the result is "Yes" then
22                        close theDoc saving no -- 关闭文档,不保存
23                    end if
24                else
25                    close theDoc saving no -- 如果文档已保存,直接关闭不保存
26                end if
27                exit repeat -- 退出循环
28            end if
29        end repeat
30    end tell
31end run
32
33on trimText(inputText)
34    return do shell script "echo " & quoted form of inputText & " | xargs"
35end trimText