Как писать плагины 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