Как писать плагины pandoc в Nodejs
Pandoc — отличный инструмент для преобразования текстовых форматов в другие текстовые форматы. Я использую его для создания версий PDF / HTML / epub / mobi из моей книги из моих файлов Markdown.
Он поставляется с плагинами (пандок-фильтрами), которые позволяют преобразовывать AST (абстрактное синтаксическое дерево) ваших текстовых файлов, чтобы вы могли создать свой собственный синтаксис и заставить его делать многое. что-либо. Идея аналогична другим известным вам средам преобразования AST, таким как jscodeshift или remark. Хотя pandoc и его плагины в основном написаны на Haskell, вы также можете написать их на JavaScript, используя Node.js.
Мы напишем и протестируем плагин, позволяющий включать исходный код из вашей файловой системы в блоки кода. Полнофункциональный плагин доступен на GitHub as pandoc-code-file-filter
.
Разработка
Плагин pandoc — это двоичный файл, который можно передать в качестве опции для запуска команды pandoc:
pandoc <args> --filter path/to/pandoc-filter-binary
Это означает, что мы начнем с настройки проекта NPM
и создания файла bin/filter.js
с преамбулой #!/usr/bin/env node
. Нам нужно установить pandoc-filter-promisified
, который включает привязки pandoc.
Наш двоичный файл использует эту библиотеку и применяет функцию action
ко всему, что pandoc отправляет нашему фильтру.
#!/usr/bin/env node
const pandoc = require('pandoc-filter-promisified')
const action = require('../src/index.js')
pandoc.stdio(action)
Если вы хотите опубликовать свой pandoc-фильтр через NPM, вам нужно сослаться на двоичный файл в package.json как
"bin": { "pandoc-code-file-filter": "bin/filter.js"}
Основная часть нашей логики будет реализована в нашем файле src/index.js
. Точкой входа плагина pandoc является файл action
. Ему передается каждый блок AST и некоторая метаинформация о происходящем преобразовании.
const fs = require('fs')
const path = require('path')
const pandoc = require('pandoc-filter-promisified')
const { CodeBlock } = pandoc
async function action(elt, pandocOutputFormat, meta) {
if (elt.t === `CodeBlock`) {
// console.warn(JSON.stringify(elt, null, 4));
const [headers, content] = elt.c
const includePath = getIncludeHeader(headers)
// it's a normal code block, no need to do anything
if (!includePath) return
// filter out the include value if another filter processes this code block
const newHeaders = filterOutOwnHeaders(headers)
let newContent = replaceWithFile(include)
return CodeBlock(newHeaders, newContent)
}
}
module.exports = action
Объект elt
описывает узел синтаксического дерева. Его тип можно проверить с помощью ключа t
, а его заголовок и содержимое можно прочитать с помощью ключа c
. Следует признать, что эти имена и ключи выглядят так, как будто их придумал кто-то, кто провел слишком много времени в Haskell. Кроме того, для этих типов нет официальной документации. Мне показалось, что проще всего проверить исходный код pandoc-filter-node и зарегистрировать результаты.
Здесь мы проверяем, является ли тип CodeBlock
, и проверяем, указан ли include
в заголовках.
function filterOutOwnHeaders(headers) {
const [_, classes, keyValuePairs] = headers
const newKeyValuePairs = keyValuePairs.filter(
([key, value]) => key !== `include`
)
const newHeaders = [headers[0], headers[1], newKeyValuePairs]
return newHeaders
}
function getIncludeHeader(headers) {
const [_, classes, keyValuePairs] = headers
const keyValuePair = keyValuePairs.find(([key, value]) => key === `include`)
if (!keyValuePair) return false
return keyValuePair.value
}
Это будет соответствовать следующему блоку кода в Markdown:
```{include=test.js}
```
После извлечения информации include
мы можем прочитать файл с помощью Node.js и заменить содержимое CodeBlock
содержимым файла.
function replaceWithFile(include) {
if (!fs.existsSync(include))
throw new Error(
`pandoc-code-file-filter: File does not exist: "${path.resolve(include)}"`
)
const fileContent = fs.readFileSync(include, 'utf8')
return fileContent
}
Замена AST выполняется простым возвратом нового узла в функции action
. Мы вызываем конструктор CodeBlock
с новыми заголовками (старые заголовки минус наш заголовок include
) и новым содержимым файла.
Тестирование
Чтобы проверить, работает ли наш фильтр, мы можем написать несколько шутливых тестов.
Наш пример файла Markdown будет иметь следующее содержимое:
``` {.javascript include=./test/examples/example.js}
Replace me
```
Файл example.js
с содержимым, которое мы хотим включить, — это обычный файл JS.
Теперь мы можем запустить pandoc, используя наш фильтр в файле Markdown, и проверить вывод pandoc, используя тестирование моментальных снимков jest. Создайте тест в test/index.test.js
:
const { execSync } = require("child_process");
function execPandocOnFile(fileName) {
const stdout = execSync(
`pandoc -s -t markdown test/examples/${fileName} --filter bin/filter.js`
);
return String(stdout);
}
test("replaces code block content with file content", () => {
const output = execPandocOnFile(`example.md`);
expect(output).toMatchSnapshot();
});
Здесь я запустил pandoc, чтобы создать еще один файл Markdown в качестве вывода. Новый вывод Markdown сохраняется в виде тестового снимка, что значительно упрощает проверку человеком, чем AST.
Теперь вы можете опубликовать фильтр pandoc в NPM, и люди смогут установить его с помощью npm -g your-pandoc-filter
.
И это все, что вам нужно знать, чтобы приступить к разработке собственных плагинов pandoc. ✨
Первоначально опубликовано на cmichel.io