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

Способ заключается в применении утилиты 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 читатель автоматически перемещается к цитируемому источнику в списке литературы. Значение параметра csl в файле default.yaml — это имя файла с правилами оформления цитируемых источников:
default.yaml
metadata: bibliography: bibliography.bib link-citations: true csl: gost-r-7-0-5-2008-numeric-iaa.csl lang: ru
gost-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-изображения кодом, размещённым внутри элемента 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 - 1] == b[j - 1]: 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, имеющих тип 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, а также особенности настройки CI в 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 за рецензирование и помощь в подготовке заметки.
