Текст книг, учебных пособий, научно-технических статей, документации, дипломных и курсовых работ часто набирается и редактируется в WYSIWYG-редакторе, таком как Microsoft Word, в том числе вследствие того, что издательства и организации требуют от авторов оформленный по ГОСТ или внутренним стандартам docx-документ. Процесс работы в Microsoft Word и аналогичных редакторах не лишён недостатков: docx-файлы трудно версионировать в git, а для объединения нескольких документов в один придётся перенумеровывать источники, рисунки, таблицы, формулы.
Альтернативой docx является LaTeX. Однако работа со стилями в LaTeX простотой и минималистичным синтаксисом не отличается, причём издательства от использования формата docx отказываться не торопятся. А инструменты в духе typst отличаются нестандартным синтаксисом языка для описания документов, причём возможность генерации сайтов в typst имеет пометку «in preview».
Markdown — популярный и удобный язык разметки, но это также и очень ограниченный формат. Поэтому задача написания в Markdown сложной технической документации по ГОСТ, научной статьи с автоматической настройкой оформления для заданного издательства или хорошо оформленного онлайн-учебника может показаться неосуществимой. В этой статье рассмотрим способ работы над научно-техническими статьями и книгами в формате Markdown на основе Docs as Code с учётом строгих ограничений на оформление, используемый @true-grue и мной при подготовке учебных материалов в РТУ МИРЭА.

Способ заключается в применении утилиты pandoc для построения дерева абстрактного синтаксиса (AST) документа с последующим переписыванием AST набором фильтров на Lua и трансляцией AST в форматы docx и pdf, соответствующие ГОСТ, а также в диалект markdown, совместимый с mdBook, для генерации онлайн-учебника.
Онлайн-версии книг, написанных с использованием описанного подхода, и репозитории с исходным кодом книг опубликованы на GitHub и GitHub Pages: книга по конфигурационному управлению, книга по разработке кроссплатформенных программмных систем.
1. Сборка простой книги
Для начала попробуем написать миниатюрную книгу в формате Markdown и транслировать исходный код книги в формат docx. Создадим новую папку для нашей книги или даже новый git-репозиторий — поскольку мы будем использовать текстовые форматы для вёрстки книги и конфигурации задействованных инструментов, получаемые результаты будет легко версионировать. Настройку проекта начнём с добавления в папку проекта книги нового файла default.yaml с базовыми настройками pandoc.
Значение параметра bibliography в файле default.yaml — это имя файла, содержащего литературные источники. Опция link-citations делает ссылки на литературные источники интерактивными — при нажатии на интерактивную ссылку в docx или pdf читатель автоматически перемещается к цитируемому источнику в списке литературы. Параметр lang позволяет указать язык документа, а значение параметра csl в файле default.yaml — это имя файла с правилами оформления цитируемых источников:
default.yaml
metadata:
bibliography: bibliography.bib
link-citations: true
csl: gost-r-7-0-5-2008-numeric-iaa.csl
lang: rugost-r-7-0-5-2008-numeric-iaa.csl
В нашей книге будет использоваться стандартное оформление списка литературы, соответствующее ГОСТ Р 7.0.5-2008, csl-файл gost-r-7-0-5-2008-numeric-iaa.csl позаимствуем из проекта GOSTdown, сохраним его в папку с нашей книгой.
Добавим также файл bibliography.bib, содержащий литературные источники в формате BibTeX. Такие сервисы, как Google Scholar и Истина, поддерживают не только поиск актуальных научных работ на заданную тему, но и автоматическое формирование ссылок на цитируемые научные статьи, книги и расшифровки конференций в формате BibTeX — поэтому целесообразно использовать именно этот формат для цитирования источников:
bibliography.bib
@inproceedings{levenshtein,
title={Двоичные коды с исправлением выпадений, вставок и замещений символов},
author={Левенштейн, Владимир Иосифович},
booktitle={Доклады Академии наук},
volume={163},
number={4},
pages={845--848},
year={1965},
organization={Российская академия наук},
url="http://mi.mathnet.ru/dan31411"
}Теперь создадим файл book.md с текстом нашей книги в формате Markdown. Текст книги будет содержать заголовок первого уровня, ссылку на источник в списке литературы @levenshtein, содержащийся в файле bibliography.bib, а также многострочную формулу на языке описания математических формул, используемом в системе компьютерной вёрстки LaTeX:
book.md
# Расстояние Левенштейна
Расстояние Левенштейна @levenshtein может быть определено как минимальное
число операций **замены**, **вставки** и **удаления** символов, необходимых
для преобразования одной последовательности символов в другую:
$$
f(a, b, i, j) = \begin{cases}
\max(i, j), & i = 0 \lor j = 0, \\
f(a, b, i-1, j-1), & a_i = b_j, \\
1 + \min \left( \begin{array}{}
f(a, b, i, j-1), \\
f(a, b, i-1, j), \\
f(a, b, i-1, j-1)
\end{array} \right), & a_i \neq b_j.
\end{cases}
$$
# Список литературыПопробуем скомпилировать нашу маленькую книгу, состоящую только из одного раздела, в файл в формате DOCX для редактора Microsoft Word. Воспользуемся pandoc — универсальным преобразователем документов, написанным на Haskell. Портативную версию утилиты pandoc в виде zip-архива с файлом pandoc.exe внутри можно скачать в разделе Releases на GitHub. В папке с исходным кодом нашей книги создадим подпапку tools и поместим в неё файл pandoc.exe. После чего выполним сборку книги, запустив pandoc со следующими параметрами:
tools/pandoc.exe book.md -d default.yaml --citeproc --from=markdown --to=docx --output=book.docxВ результате трансляции книги из формата Markdown в формат docx был создан документ book.docx с автоматически сформированным списком литературы, соответствующим ГОСТ Р 7.0.5-2008, с автоматически пронумерованными интерактивными ссылками на источники в тексте, а также с формулой в формате Office Math Markup Language (OMML), совместимом с редактором уравнений в Microsoft Word:

2. Нумерация формул и стили текста
Однако, текст скомпилированного на предыдущем шаге документа book.docx пока не соответствует принятым стандартам оформления, таким как, например, ГОСТ 7.32 2017. В частности, в book.docx используется шрифт без засечек, цветные заголовки, одинарный межстрочный интервал. Кроме того, формула не имеет номера, из-за чего ссылаться на неё в тексте затруднительно.
Фильтр pandoc-crossref добавляет в pandoc поддержку автоматической нумерации не только источников из списка литературы, но и рисунков, формул, таблиц, разделов. Портативную версию утилиты pandoc-crossref можно скачать в разделе Releases на GitHub. Поместим скачанный файл pandoc-crossref.exe в папку tools внутри папки с исходным кодом нашей книги — так фильтр станет доступен по относительному пути tools/pandoc-crossref.exe. Создадим новый файл crossref-docx.yaml, в этом файле будут находиться настройки инструмента pandoc-crossref, специализированные для Microsoft Word, такие как префиксы и шаблоны полных и сокращённых названий рисунков, таблиц, формул:
crossref-docx.yaml
figureTitle: Рисунок
figPrefix: рис.
titleDelim: .
tableTitle: Таблица
tblPrefix: табл.
numberSections: true
sectionsDepth: 3
secHeaderDelim: .
tableEqns: true
eqnPrefixTemplate: ($$i$$)
eqnBlockInlineMath: true
eqnBlockTemplate: |
`
<w:pPr>
<w:tabs>
<w:tab w:val="center" w:leader="none" w:pos="4680" />
<w:tab w:val="right" w:leader="none" w:pos="9960" />
</w:tabs>
</w:pPr>
<w:r>
<w:tab />
</w:r>
`{=openxml} $$t$$
`
<w:r>
<w:tab />
</w:r>
`{=openxml} $$i$$Параметр eqnBlockTemplate задаёт шаблон отображения формул в документе Microsoft Word в формате OOXML (Office Open XML, стандарт ECMA-376) — в приведённой конфигурации сама формула выравнивается по центру страницы, а номер формулы — по правому краю страницы.
template.docx
На следующем шаге добавим в папку с книгой файл-шаблон template.docx с настройками стилей текста. Файл-шаблон со стилями, настроенными для соответствия ГОСТ и требованиям издательства, с которым мы сотрудничали, можно найти в нашем репозитории на GitHub, скачать template.docx и поместить в папку с Вашей книгой. Стили текста в Microsoft Word настраиваются вручную, при помощи специального графического интерфейса, доступного в меню "Стили текста" на вкладке "Главная", это меню также можно открыть сочетанием клавиш Ctrl+Shift+Alt+S. В template.docx можно настроить и колонтитулы с нумерацией страниц, их унаследует docx-файл при сборке. Настройка стилей — кропотливая ��абота, но эту настройку достаточно провести один раз, после чего можно переиспользовать файл-шаблон в разных проектах без изменений.
Важно отметить, что стили Microsoft Word, определённые в файле template.docx, позволяют настроить только внешний вид блоков текста разных типов, но не изменить сам текст. Однако, в некоторых стандартах и организациях требуется, например, набирать заголовки разделов заглавными буквами. Для преобразования текста заголовков разделов в верхний регистр напишем на языке Lua фильтр upper.lua, используя API pandoc, позволяющее переписывать дерево абстрактного синтаксиса (AST) документа в процессе его трансляции из исходного формата в целевой. Код фильтра upper.lua поместим в папку tools. Этот фильтр выполнит обход AST и преобразует текст Str в заголовках первого уровня Header (# Пример) в верхний регистр при помощи функции upper из модуля pandoc.text:
tools/upper.lua
local text = require('text')
function Header(el)
if el.level == 1 then
return pandoc.walk_block(el, {
Str = function(el)
return pandoc.Str(text.upper(el.text))
end
})
end
endКаждый заголовок первого уровня располагается в начале нового раздела, а новый раздел, в свою очередь, должен начинаться с новой страницы. Вследствие этого в процессе обхода AST pandoc в наборах блоков Blocks, из которых состоит текст книги, перед каждым заголовком первого уровня будем помещать разрыв страницы, обозначаемый как \newpage. Поместим следующий фрагмент кода на Lua в созданный ранее файл tools/upper.lua:
function Blocks(blocks)
local hblocks = {}
for i, el in pairs(blocks) do
if el.t == "Header" and el.level == 1 then
table.insert(hblocks, pandoc.RawBlock("tex", "\\newpage"))
end
table.insert(hblocks, el)
end
return hblocks
endПри помощи стороннего фильтра pagebreak.lua, который также необходимо поместить в папку tools, команда \newpage будет автоматически заменена на следующую команду в формате OOXML при сборке книги в формат Microsoft Word: <w:p><w:r><w:br w:type="page"/></w:r></w:p>.
Обновим содержимое md-файла с нашей книгой book.md — теперь мы можем добавить необходимые пояснения к формуле, сославшись на неё в тексте. С новыми настройками разделы в книге будут автоматически нумероваться, для отключения нумерации заголовок явно помечается последовательностью символов {-}:
book.md
# Расстояние Левенштейна
Расстояние Левенштейна @levenshtein может быть определено как минимальное
число операций **замены**, **вставки** и **удаления** символов, необходимых
для преобразования одной последовательности символов в другую:
$$
f(a, b, i, j) = \begin{cases}
\max(i, j), & i = 0 \lor j = 0, \\
f(a, b, i-1, j-1), & a_i = b_j, \\
1 + \min \left( \begin{array}{}
f(a, b, i, j-1), \\
f(a, b, i-1, j), \\
f(a, b, i-1, j-1)
\end{array} \right), & a_i \neq b_j.
\end{cases}
$$ {#eq:leven}
В формуле @eq:leven $a$ и $b$ – это сравниваемые последовательности символов,
$i$ – это номер символа в строке $a$, $j$ – номер символа в строке $b$.
# Список литературы {-}Скомпилируем нашу обновлённую книгу:
tools/pandoc.exe book.md -d default.yaml `
--metadata crossrefYaml=crossref-docx.yaml `
--filter tools/pandoc-crossref.exe `
--lua-filter tools/upper.lua `
--lua-filter tools/pagebreak.lua `
--citeproc `
--reference-doc=template.docx `
--from=markdown `
--to=docx `
--output=book.docxПри необходимости можно также на основе заголовков первого (# Пример) и второго (## Пример) уровней автоматически сгенерировать оглавление, добавив опцию --toc при сборке docx-документа. Обновлённый файл book.docx теперь выглядит так:

3. Изображения как код
Традиционный подход к встраиванию изображений в Markdown — сохранить картинку рядом с md-файлом и поместить в md-файл ссылку на изображение, используя синтаксис . Однако, с таким подходом для внесения изменений в изображение потребуется графический редактор. Кроме того, с таким подходом к редактированию изображений затруднительно их версионировать в системе контроля версий git, а результат работы утилиты git diff оказывается не информативным.
Вследствие этого многие сервисы, поддерживающие онлайн-редактирование и просмотр Markdown-документов, такие как, например, GitHub, поддерживают подход «изображения как код» (diagrams as code). Для решения той же проблемы в нашем окружении мы сделали фильтр pysvg, позволяющий генерировать схемы и диаграммы в формате SVG при помощи кода на языке Python.
Фильтр pysvg состоит из двух компонентов — непосредственно фильтра pandoc на языке Lua pysvg.lua и библиотеки с вспомогательными функциями pysvg.py. Фильтр pysvg.lua обходит AST документа и заменяет каждый блок кода CodeBlock, помеченный классом .pysvg, на рисунок, содержащий результат генерации SVG-изображения кодом на языке Python, размещённым внутри элемента CodeBlock. Поместим реализацию фильтра pysvg.lua в папку tools:
tools/pysvg.lua
local function diagram_options(cb)
local attribs = cb.attributes or {}
local caption = attribs.caption and pandoc.read(attribs.caption).blocks
local image_attrs = {}
for attr, value in pairs(attribs) do
if attr ~= "caption" then image_attrs[attr] = value end
end
return {
alt = caption and pandoc.utils.blocks_to_inlines(caption) or {},
caption = caption,
figure_attrs = {id = cb.identifier},
image_attrs = image_attrs
}
end
function CodeBlock(el)
if el.attr.classes[1] == "pysvg" then
-- Сгенерируем SVG при помощи программы на Python и модуля pysvg.py.
-- В качестве имени файла используем хэш-значение от его содержимого.
local header = "from tools.pysvg import *\n"
local svg = pandoc.pipe("python", {"-"}, header .. el.text)
local fname = pandoc.sha1(svg) .. ".svg"
pandoc.mediabag.insert(fname, "image/svg+xml", svg)
-- Заменим CodeBlock в AST на рисунок Figure при наличии подписи.
-- При отсутствии подписи разместим рисунок в AST как обычный текст.
local options = diagram_options(el)
local image = pandoc.Image(options.alt, fname, "", options.image_attrs)
local plain = pandoc.Plain {image}
return options.caption and
pandoc.Figure(plain, options.caption, options.figure_attrs) or
plain
end
endВ функции diagram_options все атрибуты, кроме подписи к рисунку caption, будем считать атрибутами изображения. С таким подходом становится возможным задать блоку кода, помеченному классом .pysvg, такие атрибуты, как width или height, причём значения этих атрибутов унаследует сгенерированное SVG-изображение. В том случае, если описание изображения не задано, CodeBlock в AST будет заменён на картинку без подписи pandoc.Image. При наличии атрибута caption у блока кода в тексте книги будет размещён полноценный рисунок с подписью и заданным пользователем уникальным идентификатором, который pandoc-crossref затем превратит в порядковый номер рисунка.
Библиотека pysvg.py, в свою очередь, представляет из себя модуль с вспомогательными функциями, которые могут использоваться для процедурной генерации изображений в формате SVG кодом на языке Python, размещённым в тексте книги. Так, например, функция dot в приведённом ниже файле pysvg.py в отдельном процессе запускает утилиту командной строки инструмента для визуализации графов graphviz и перенаправляет сгенерированный при помощи graphviz текст в формате SVG в стандартный вывод. Поместим реализацию библиотеки pysvg.py в папку tools:
tools/pysvg.py
import sys
import subprocess
sys.stdout.reconfigure(encoding='utf-8')
def dot(s):
p = subprocess.run(['dot', '-Tsvg'],
input=s.encode('utf8'),
capture_output=True)
print(p.stdout.decode('utf8'))
Аналогичным функции dot образом могут быть реализованы, например, функции mermaid, plantuml, matplot для генерации графов и диаграмм в формате SVG при помощи инструментов Mermaid, PlantUML и matplotlib, однако для задач авторов статьи возможностей визуализатора графов graphviz пока оказалось достаточно.
Обновим исходный код нашей миниатюрной книги, добавим в текст книги пример программы и пример диаграммы на языке описания графов dot, использующемся в graphviz:
# Расстояние Левенштейна
Расстояние Левенштейна @levenshtein может быть определено как минимальное
число операций **замены**, **вставки** и **удаления** символов, необходимых
для преобразования одной последовательности символов в другую:
$$
f(a, b, i, j) = \begin{cases}
\max(i, j), & i = 0 \lor j = 0, \\
f(a, b, i-1, j-1), & a_i = b_j, \\
1 + \min \left( \begin{array}{}
f(a, b, i, j-1), \\
f(a, b, i-1, j), \\
f(a, b, i-1, j-1)
\end{array} \right), & a_i \neq b_j.
\end{cases}
$$ {#eq:leven}
В формуле @eq:leven $a$ и $b$ – это сравниваемые последовательности символов,
$i$ – это номер символа в строке $a$, $j$ – номер символа в строке $b$.
Реализуем формулу @eq:leven на Python, поместим программу в файл `lev.py`:
```python
def diff(a, b, i, j):
if i == 0 or j == 0:
return max(i, j)
if a[i] == b[j]:
return diff(a, b, i - 1, j - 1)
return 1 + min(diff(a, b, i, j - 1),
diff(a, b, i - 1, j),
diff(a, b, i - 1, j - 1))
a, b = input(), input()
print(diff(a, b, len(a), len(b)))
```
Схема организации ввода-вывода в `lev.py` показана на @fig:levio:
```{#fig:levio .pysvg caption="Ввод-вывод в `lev.py`" width=70%}
dot('''
digraph G {
edge [arrowhead=none]
rankdir=LR
ranksep=0.3
1 [label="Строки", shape=none]
2 [label="stdin", shape=rarrow]
3 [label="lev", shape=circle, fixedsize=shape, style=filled]
4 [label="stdout", shape=rarrow]
5 [label="Число", shape=none]
1 -> 2
4 -> 5
subgraph cluster_0 {
graph [style=dashed]
2 -> 3
3 -> 4
}
}
''')
```
# Список литературы {-}Скомпилируем книгу, воспользовавшись следующей командой:
tools/pandoc.exe book.md -d default.yaml `
--metadata crossrefYaml=crossref-docx.yaml `
--lua-filter tools/pysvg.lua `
--filter tools/pandoc-crossref.exe `
--lua-filter tools/upper.lua `
--lua-filter tools/pagebreak.lua `
--citeproc `
--reference-doc=template.docx `
--from=markdown `
--to=docx `
--output=book.docxТеперь в редакторе Microsoft Word книга выглядит так:

4. Экспорт в формат PDF
Для автоматического преобразования книги в формат pdf из ранее собранного при помощи pandoc файла в формате docx воспользуемся модулем docx2pdf из реестра пакетов Python. Установить этот модуль можно как в виртуальное окружение, так и глобально. Для глобальной установки достаточно воспользоваться следующей командой:
pip install docx2pdfСоздадим утилиту командной строки doc2pdf.py в папке tools, принимающую на вход имя файла в формате docx и расположение файла в формате pdf, который должен быть сформирован нашей утилитой. Для преобразования docx в pdf воспользуемся функцией convert из модуля docx2pdf. Для корректной работы модуля docx2pdf на Windows или macOS должен быть установлен Microsoft Word, docx2pdf преобразует файл в формате docx в формат pdf посредством взаимодействия с API Microsoft Word:
tools/doc2pdf.py
import sys
from docx2pdf import convert
convert(sys.argv[1], sys.argv[2])Процесс преобразования Markdown-книги в PDF состоит из двух этапов -- сборки docx-документа при помощи pandoc при помощи описанной в предыдущем разделе команды и преобразования docx в pdf при помощи утилиты командной строки tools/doc2pdf.py:
python tools/doc2pdf.py book.docx book.pdfПолучим следующий результат:

5. Сборка статического сайта mdBook
Теперь попробуем превратить нашу книгу в полноценное веб-приложение при помощи генератора статических сайтов с документацией mdBook. Начнём с создания в папке с книгой файла с конфигурацией генератора mdBook:
book.toml
[book]
title = "Книга"
language = "ru"
src = "src"
[build]
build-dir = "build"
[output.html]
mathjax-support = true
no-section-label = trueОпция no-section-label отключает автоматическую нумерацию разделов, в нашей книге разделы пронумерует фильтр pandoc-crossref на этапе сборки книги в формат mdBook. Опция mathjax-support включает поддержку визуализации формул на языке разметки LaTeX в веб-браузерах. Опция src содержит путь к папке с результатом компиляции исходного кода книги при помощи pandoc в формат, совместимый с mdBook, особенности которого мы рассмотрим ниже. А опция build-dir, в свою очередь, содержит путь к папке, в которую mdBook сохранит сгенерированные статические HTML-страницы.
Мы не можем применить генератор mdBook к файлу с исходным кодом нашей книги book.md напрямую, без предварительной обработки, поскольку в mdBook используется другой синтаксис, например, для математических формул, встроенных в текст, кроме того, mdBook ничего не знает о генераторе SVG-изображений pysvg. Поэтому при генерации сайта мы сначала обработаем файл book.md при помощи pandoc, pandoc-crossref и написанных нами фильтров, и только после этого сгенерируем статические HTML-страницы.
Создадим в папке с книгой новый файл с конфигурацией фильтра pandoc-crossref crossref.yaml. Содержимое этого файла идентично содержимому конфигурационного файла crossref-docx.yaml за исключением удалённой строки с определением шаблона формул eqnBlockTemplate в формате OXML для Microsoft Word. Без переопределения параметра eqnBlockTemplate будет использоваться шаблон формул по умолчанию:
crossref.yaml
figureTitle: Рисунок
figPrefix: рис.
titleDelim: .
eqnPrefixTemplate: ($$i$$)
tableTitle: Таблица
tblPrefix: табл.
numberSections: true
sectionsDepth: 3
secHeaderDelim: .
tableEqns: trueДля математических формул, встроенных в текст, в mdBook используется синтаксис \(x\) вместо синтаксиса $x$. Для учёта этой особенности создадим в папке tools новый Lua-фильтр mdbook.lua и добавим правило переписывания вершин AST pandoc, имеющих тип Math:
tools/mdbook.lua
function Math(el)
if el.mathtype == "InlineMath" then
return pandoc.Str('\\(' .. el.text .. '\\)')
end
return el
endКаждый источник из списка литературы, помеченный классом csl-entry, представим в виде параграфа с идентификатором <p id="..."></p>, чтобы на параграф можно было сослаться в URL. Кроме того, пустые блоки Div с идентификатором будем удалять из AST, заменяя их на их содержимое. Добавим в файл tools/mdbook.lua правило переписывания вершин AST типа Div:
function Div(el)
-- Замена источников в списке литературы на параграфы с id.
-- После этого исправления заработают ссылки на источники
-- из списка литературы в веб-версии книги.
if el.classes:includes('csl-entry') then
local tree = pandoc.Pandoc(el.content[1].content)
local text = pandoc.write(tree, 'html'):gsub('\n+', ' ')
local html = '<p id="' .. el.identifier .. '">' .. text .. '</p>'
return pandoc.RawBlock('html', html)
end
-- Раскрытие содержимого блоков Div с идентификатором.
if el.identifier ~= "" then
return el.content
end
return el
endСоздадим в папке с книгой подпапку src, в неё поместим конфигурационный файл SUMMARY.md с перечнем страниц книги. Поместим в созданный файл src/SUMMARY.md следующее содержимое в формате Markdown:
src/SUMMARY.md
- [Расстояние Левенштейна](page.md)Скомпилируем книгу в представление в формате Markdown, совместимое с mdBook:
tools/pandoc.exe book.md -d default.yaml `
--metadata crossrefYaml=crossref.yaml `
--lua-filter tools/pysvg.lua `
--filter tools/pandoc-crossref.exe `
--citeproc `
--lua-filter=tools/mdbook.lua `
--extract-media=img `
--from=markdown `
--to=markdown-citations-grid_tables-implicit_figures `
--output=src/page.mdПереместим папку img со сгенерированными и сохранёнными на диск изображениями из book.md в папку src, сгенерированные в процессе трансформации AST pandoc изображения были сохранены в папку img за счёт использования опции утилиты pandoc --extract-media со значением img. После этого поместим в папку tools портативную версию генератора статических сайтов с документацией mdBook, скачать которую можно со страницы Releases на GitHub, и выполним сборку сайта:
mv img src
tools/mdbook.exe serveКоманда serve генератора mdBook выполняет сборку проекта в набор статических HTML-файлов, а также запускает отладочный веб-сервер, доступный по адресу http://localhost:3000. Наша онлайн-книга в браузере теперь выглядит так:

6. Сборочный скрипт
Запоминать команды, использованные нами для сборки книги в виде docx, pdf и статического сайта затруднительно, поэтому полезно воспользоваться всеми преимуществами подхода Docs as Code и написать сценарий сборки для, например, системы сборки GNU make. Создадим в корне папки с исходным кодом книги файл с именем Makefile и следующим содержимым:
Makefile
all: pdf web
docx:
tools/pandoc.exe book.md -d default.yaml \
--metadata crossrefYaml=crossref-docx.yaml \
--lua-filter tools/pysvg.lua \
--filter tools/pandoc-crossref.exe \
--lua-filter tools/upper.lua \
--lua-filter tools/pagebreak.lua \
--citeproc \
--reference-doc=template.docx \
--from=markdown \
--to=docx \
--output=book.docx
pdf: docx
python tools/doc2pdf.py book.docx book.pdf
web:
tools/pandoc.exe book.md -d default.yaml \
--metadata crossrefYaml=crossref.yaml \
--lua-filter tools/pysvg.lua \
--filter tools/pandoc-crossref.exe \
--citeproc \
--lua-filter=tools/mdbook.lua \
--extract-media=img \
--from=markdown \
--to=markdown-citations-grid_tables-implicit_figures \
--output=src/page.md
move img src
tools/mdbook.exe serveТеперь для сборки книги в требуемый формат достаточно написать в командной строке команду make формат. Например, для сборки docx-файла для сдачи в издательство достаточно воспользоваться командой make docx, а для сборки статического веб-сайта — командой make web. Команда make all, в свою очередь, выполняет сборку книги во всех поддерживаемых форматах, и может использоваться для CI/CD.
Представленный подход, по нашему опыту, упрощает совместную работу над книгами в git-репозиториях и позволяет, прилагая минимум усилий, адаптировать литературу под требования издательства, а также объединять несколько книг в один большой онлайн-учебник для поддержки учебного курса.
Дальнейшее изучение
За рамками статьи остался Python-скрипт mdbook.py, разбивающий большую книгу на разделы-страницы на основе заголовков 2-го уровня (## Пример) и автоматически формирующий файл с оглавлением SUMMARY.md в формате mdBook, а также особенности настройки GitHub Actions для автоматической сборки и публикации онлайн-версии книги. Оставим эти и другие файлы из git-репозитория онлайн-учебника по курсу конфигурационного управления для самостоятельного изучения заинтересованным читателем.

Перечислим также альтернативные подходы к подготовке книг и документации:
Jaan Tollander de Balsch, Scientific Writing with Markdown, 2020.
Дмитрий Павлов, Алёна Водолагина, Даниил Аксим, GOSTdown — набор шаблонов и скриптов для автоматической вёрстки документов по ГОСТ 19.xxx (ЕСПД) и ГОСТ 7.32 (отчёт о научно-исследовательской работе) в форматах docx из файлов текстовой разметки Markdown, ИПА РАН, 2024.
Андрей Акиньшин, LaTeX-шаблон для русской кандидатской диссертации и её автореферата, GitHub, 2025.
Иван Кочуркин, Статьи — это тоже исходный код {, Habr, 2020.
Спасибо автору идеи представленного в статье способа организации проектов сборки научно-технических статей и учебников по Docs as Code с учётом строгих ограничений на оформление Петру Советову @true-grue за рецензирование и помощь в подготовке заметки.
