Pandocin suodattimien käyttö

Aiemmassa postauksessa tarkastelin Pandocin AST:tä. Siihen voi esimerkiksi tallentaa CodeBlock-noden, johon sisältyy ohjelmakoodia. Esittelen tässä lyhyesti, kuinka voidaan vaikuttaa yksityiskohtaisesti siihen, miltä AST:stä renderöity lopputulos näyttää LaTeXilla. Käytännön tasolla, CodeBlock pitää muuttaa RawBlock-nodeksi käyttämällä Pandocin suodattimia.

Käytetään tässä esimerkkinä seuraavaa SQL-kyselyä:

```{#sql1 .sql caption="Hankala SQL-kysely: 1+1."}
SELECT 1+1";
```

Tämä näkyy Pandocin AST:ssä tallennettuna:

[CodeBlock ("sql1",["sql"],[("caption","Hankala SQL-kysely: 1+1.")]) "SELECT 1+1;"]

Teknisesti siinä on kaikki tarpeellinen. sql1 on lohkon id-numero ja ["sql"] viittaa periaatteessa lohkon luokkiin, käytännössä sillä tunnistetaan mikä ohjelmointikieli on kyseessä. Id-numeron ja luokkien jälkeen annetaan key-value arvoja, jotka voivat olla mitä tahansa. Esimerkiksi caption-avain on minun itseni keksimä eikä se tee yhtään mitään, vielä, mutta se voisi esimerkiksi kirjoittaa listauksen otsikon kun node renderöidään LaTeX-formaattiin. Lopuksi tulee varsinainen lohkon sisältö.

Nyt tarkoituksena olisi saada tämä AST:ssä näkyvä node kirjoitettua esimerkiksi LaTeX-formaattiin. Pandocissa on valmiiksi LaTeX-kirjoittaja, ja tämä node sillä renderöitynä näyttää seuraavalta.

\hypertarget{sql1}{
\label{sql1}}
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{SELECT} \DecValTok{1}\NormalTok{+}\DecValTok{1}\NormalTok{;}
\end{Highlighting}
\end{Shaded}

Komennot, joilla konversiot siis tehtiin, ovat

pandoc -f markdown -t native codeblock.md > codeblock.native
pandoc -f native -t latex codeblock.native > codeblock.tex

Haluaisin käyttää LaTeXissa tuon lohkon renderöimiseen jotakin muuta ympäristöä kuin tarjottua Shaded ja Highlighting. Esimerkiksi minted. Tätä tavoitetta voisi lähestyä varmaan useammallakin eri tavalla. Ensimmäisenä tulee mieleen että jossakin syvällä lähdekoodin syövereissä täytyy olla Haskellilla naputeltuna Pandocin LaTeX-writer, jota voisi muokata. Tällaisia kirjoittajia voi varmasti myös tehdä itse uusia. Tämä tuskin kuitenkaan on oikea tapa edetä, sillä pääpiirteittäin LaTeX kirjoitetaan aivan oikein. Tässä halutaan nyt uudelleenkirjoittaa vain yksittäinen node.

AST:n nodesta pitäisi päästä vaikkapa seuraavaan:

\begin{listing}
\caption{Hankala SQL-kysely: 1+1.}
\begin{tcolorbox}
\begin{minted}{sql}
SELECT 1+1;
\end{minted}
\end{tcolorbox}
\label{listing:sql1}
\end{listing}

Kuten edellisessä kirjoituksessa esiselvityksessä kävi ilmi, ei Pandocin AST:ssä ole kaikille mahdollisille rakenteille omia tyyppejä, joten nähdäkseni ainoa järkevä vaihtoehto on vaihtaa CodeBlock-tyyppi RawBlock-tyyppiin, parametrilla latex, sekä tallentaa LaTeX-koodi suoraan siihen. Eli, uusi solmu AST-puussa olisi seuraavanlainen:

[RawBlock (Format "latex") "
\\begin{listing}
\\caption{Hankala SQL-kysely: 1+1.}
\\begin{tcolorbox}
\\begin{minted}{sql}
SELECT 1+1;
\\end{minted}
\\end{tcolorbox}
\\label{listing:sql1}
\\end{listing}"]

Pandoc tukee suodattimia, joiden tarkoituksena on muokata AST:tä ennen sen kirjoittamista. Suodattimia voi kirjoittaa millä tahansa ohjelmointikielellä, ja ne ottavat sisälle AST:n JSON-version ja antavat ulos uuden version AST:stä, jossa filtteriä on käytetty.

Tässä tapauksessa AST on JSON-formaatissa:

{
  "blocks": [
    {
      "t": "CodeBlock",
      "c": [
        ["sql1", ["sql"], [["caption", "Hankala SQL-kysely: 1+1."]]],
        "SELECT 1+1;"
      ]
    }
  ],
  "pandoc-api-version": [1, 17, 5, 1],
  "meta": {}
}

Tehtäväksi jää tehdä esimerkiksi Python-skripti, joka lukee JSON-tiedoston ja muokkaa sitä. Yksinkertainen implementaatio tähän olisi esimerkiksi:

import json
import sys

__template__ = """
\\begin{listing}
\\caption{% raw %}{%s}{% endraw %}
\\begin{tcolorbox}
\\begin{minted}{% raw %}{%s}{% endraw %}
%s
\\end{minted}
\\end{tcolorbox}
\\label{listing:%s}
\\end{listing}
"""

ast = json.loads(sys.stdin.read())
for block in ast["blocks"]:
    if block["t"] == "CodeBlock":
        ((identifier, classes, kvpairs), code) = block["c"]
        block["t"] = "RawBlock"
        args = dict(kvpairs)
        params = (args["caption"], classes[0], code, identifier)
        block["c"] = ["latex", __template__.strip() % params]

print(json.dumps(ast))

Nyt saamme AST:n oikeaan muotoon

cat codeblock.json | ./pandoc-codeblock-minted.py | python -m json.tool
{
  "blocks": [
    {
      "t": "RawBlock",
      "c": [
        "latex",
        "\\begin{listing}\n\\caption{Hankala SQL-kysely: 1+1.}\n\\begin{tcolorbox}\n\\begin{minted}{sql}\nSELECT 1+1;\n\\end{minted}\n\\end{tcolorbox}\n\\label{listing:sql1}\n\\end{listing}"
      ]
    }
  ],
  "pandoc-api-version": [1, 17, 5, 1],
  "meta": {}
}

Ja nyt kun käytetään tätä skriptiä Pandocin suodattimena, saadaan odotettu lopputulos:

pandoc -f markdown -t latex --filter=./pandoc-codeblock-minted.py codeblock.md
\begin{listing}
\caption{Hankala SQL-kysely: 1+1.}
\begin{tcolorbox}
\begin{minted}{sql}
SELECT 1+1;
\end{minted}
\end{tcolorbox}
\label{listing:sql1}
\end{listing}

Suodatin toimii, mutta se on hieman purkkavirityksen oloinen. Vähintäänkin pitäisi tarkastella, onko kaikki parametrit annettu ja sen perusteella muokata formaattia. Esimerkiksi, jos käyttäjä ei ole antanut otsikkoa, ohjelmointikielen nimeä tai tunnistetta, pitäisi vasteesta poistaa asianmukaiset rivit. Lisäksi pitäisi jotenkin kontrolloida, että suodatinta käytetään ainostaan mikäli AST:tä ollaan kirjoittamassa LaTeX-formaattiin.

Löytyy myös valmiita projekteja kuten pandoc-filters ja panflute joita ilman muuta kannattaa hyödyntää, jos suodattimia kirjoittaa JSON-formaatissa esimerkiksi Pythonilla. Valmiitakin suodattimia löytyy, ja esimerkiksi minted-ympäristön kirjoittamiseen on valmis suodatin.

Kun AST:tä parsitaan JSONista, ei ohjelmointikielellä periaatteessa ole merkitystä. Voi käyttää sitä ohjelmointikieltä minkä parissa parhaiten viihtyy. Jotkin ohjelmointikielet voivat olla toista parempia JSON-datan käsittelyssä. Python on tässä varmastikin edukseen.

Eräs mielenkiintoinen ajatus olisi ajaa dokumentissa olevia koodiblokkeja sitä mukaan kun niitä tulee AST:ssä vastaan. Tämä edellyttäisi sopivan eristetyn leikkikentän rakentamista, joten suodatin on silloin järkevintä toteuttaa sillä ohjelmointikielellä mitä aikoo suorittaa.

Pandociin on rakennettu myös Lua-suodattimet. Lua on käännetty ohjelmaan sisälle, mistä on tiettyjä tehokkuusetuja, sillä Lua-suodattimilla ei JSON-tiedostoa tarvitse parsia. Jos Lua-ohjelmointikieltä sattuu osaamaan tai sitä haluaa oppia, niin Lua olisi se parempi tapa rakennella suodatin.