Pandoc-konvertoijan datatyypeistä

Pandoc on universaali dokumenttikonvertoija. Sillä voi kääntää dokumentteja eri formaattien väilllä, esimerkiksi blogimaailmasta tutusta Markdown-formaatista HTML-formaattiin, ja toisaalta samasta lähteestä myös LaTeX-formaattiin, ja niin edelleen. Kuinka se sen tekee? Pandoc parsii dokumentin abstraktiksi syntaksipuuksi tai AST:ksi (englanniksi “abstract syntax tree”), jonka jälkeen se kirjoittaa tämän edelleen haluttuun formaattiin. Olen tutkiskellut tuota Pandocin AST:tä sekä eri dokumenttien välisiä konversioita ja julkaisen löydökseni tässä blogissa.

Olen pitkään tutkinut mahdollisuutta kirjoittaa monella alustalla yhtä aikaa. Tieteellisessä maailmassa kirjoitellaan LaTeXilla ja internet-maailmassa Markdownilla. Myös muitakin formaatteja on, esimerkiksi Microsoftin formaatit ja reStructuredText, mutta pääosin näin. Täydellisessä formaatissa yhdistyisi LaTeXin monipuolisuus ja markdownin luettavuus. Lisäksi toivelistalla on että siinä voisi evaluoida ohjelmakoodia ja testata sen tuloste.

Pandoc ei varsinaisesti ole formaatti mutta sen avulla nämä tavoitteet alkavat yhdistymään. On nimittäin niin, että Pandocin AST:n ja sitä kautta dokumentin formaatin voi kuvailla JSON-formaatissa ja sitä voi suoraan manipuloida filttereillä sopivan lopputuloksen saavuttamiseksi. Joten, valitsemalla tai ohjelmoimalla sopivia filttereita, tehtävä on siis periaatteessa mahdollinen, vai onko?

Pandocin toimintaa voi tutkia komennolla

pandoc -s markdown -t latex tiedosto.md

tai

pandoc -s latex -t markdown tiedosto.tex

Mentiinpä kumpaan suuntaan tahansa, olisi toivottavaa että pandoc osaisi parsia tuloksen AST:hen kunnolla. Käytännössä kuitenkin varmaan on niin, että LaTeXista on helpompaa tehdä markdown kuin toisin päin. Perustavanlaatuisia kysymyksiä siis on (ainakin) kaksi:

  1. kuinka saada tieto Pandocin AST:hen, ja
  2. kuinka saada tieto kirjoitettua Pandocin AST:stä.

Tarkastelen tässä yksityiskohtaisesti kuinka esittää ohjelmakoodia, kuvia ja yhtälöitä. Muitakin rakenteita AST:ssä toki on, mutta nämä ovat ne yleisimmät jotka aiheuttavat päänvaivaa eri formaattien välillä.

Koodiblokit (CodeBlock)

Tarkastellaan ensimmäisenä koodiblokkia. Kirjoitetaan yksinkertainen SQL-kysely:

```sql
SELECT "Hello, world!";
```

Tallennetaan tiedostoon, esimerkiksi hello.md. Pandocin konversio sen natiiviin AST-formaattiin saadaan:

pandoc -f markdown -t native hello.md
[CodeBlock ("",["sql"],[]) "SELECT \"Hello, world!\";"]

Tässähän heti herää kysymys: mitä ovat nuo tyhjät placeholderit CodeBlock-nodessa Pandocin AST:ssä?

LaTeX-formaatissa tämä olisi

pandoc -f markdown -t latex hello.md
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{SELECT} \OtherTok{"Hello, world!"}\NormalTok{;}
\end{Highlighting}
\end{Shaded}

Palaamme tähän myöhemmin. Keskitytään ensin kuitenkin tuohon natiiviin formaattiin.

Komennolla

pandoc -f markdown -t markdown hello.md

Saamme saman Markdown-tiedoston kirjoitettua uudelleen. Näyttäisi, että Pandoc käyttää hieman modifoitua versiota normaalista Markdownista.

```{.sql}
SELECT "Hello, world!";
```

Vaatii hieman salapoliisityötä selvittää, mitä nuo natiiviformaatin puuttuvat palaset ovat. Konvertoidaan Pandocin AST JSON-formaattiin:

pandoc -f markdown -t json hello.md | python -m json.tool
{
  "blocks": [
    {
      "t": "CodeBlock",
      "c": [["", ["sql"], []], "SELECT \"Hello, world!\";"]
    }
  ],
  "pandoc-api-version": [1, 17, 5, 1],
  "meta": {}
}

AST koostuu listasta "blocks", jossa on lueteltu objekti avaimilla t ja c. Googlettelemalla selviää, että nuo c (content?) listan palaset ovat (id, classes, namevals). Muokataan annettua JSON-tiedostoa hieman:

{
  "blocks": [
    {
      "t": "CodeBlock",
      "c": [
        [
          "my-id",
          ["sql", "text"],
          [
            ["a", "b"],
            ["c", "d"]
          ]
        ],
        "SELECT \"Hello, world!\";"
      ]
    }
  ],
  "pandoc-api-version": [1, 17, 5, 1],
  "meta": {}
}

Nyt käännettäessä AST takaisin Markdowniksi:

pandoc -f json -t markdown hello.json
```{#my-id .sql .text a="b" c="d"}
SELECT "Hello, world!";
```

Natiivina tämä on:

pandoc -f json -t native hello.json
[CodeBlock ("my-id",["sql","text"],[("a","b"),("c","d")]) "SELECT \"Hello, world!\";"]

On siis ainakin onnistuneesti löydetty, kuinka tuo Pandocin AST:n CodeBlock voidaan täyttää Markdown-formaatilla!

Enempää ei voida parsimiselle tehdä. Toivottavaa olisi, että esimerkiksi markdown

```{#listing:lst1 .sql caption="SQL-kysely, "Hello, World!""}
SELECT "Hello, world!";
```

Eli natiiviformaattina

[CodeBlock ("listing:lst1",["sql"],[("caption","SQL-kysely, \"Hello, World!\"")]) "SELECT \"Hello, world!\";"]

Muuttuisi muotoon

\begin{listing}
\caption{SQL-kysely "Hello, World!"}
\begin{tcolorbox}
\begin{minted}{sql}
SELECT "Hello, world!";
\end{minted}
\end{tcolorbox}
\label{listing:lst1}
\end{listing}

Pandocin AST:ssä ei varmaankaan voi helposti esitellä uusia lohkoja, joten yllä kuvattua listing -> tcolorbor -> minted rakennetta ei voi kuvata suoraan AST:hen. Mainittu LaTeX-koodi pitää siis jotenkin ujuttuu AST:n nodeen.

On nimittäin niin, että AST:stä löytyy RawBlock, johon voi tallentaa suoraan LaTeX-koodia:

RawBlock (Format "latex") "\\begin{listing}\n\\caption{SQL-kysely \"Hello, World!\"}\n\\begin{tcolorbox}\n\\begin{minted}{sql}\nSELECT \"Hello, world!\";\n\\end{minted}\n\\end{tcolorbox}\n\\label{listing:lst1}\n\\end{listing}"]

Nämä eri AST:n rakennuspalikat on määritelty Pandocin sisäisessä projektissa pandoc-types, ja projektin dokumentaatiosta voi myös yrittää selvitellä näiden rakennuspalasten nimiä ja tyyppejä. Esimerkiksi tämä mainittu CodeBlock rakentuu seuraavasti:

data CodeBlock Attr Text
Code block (literal) with attributes

Ja tässä Attr on

type Attr = (Text, [Text], [(Text, Text)])
Attributes: identifier, classes, key-value pairs

Inlinekoodi menee seuraavasti:

koodi `f(x) = x`

Joka Pandocin natiivina

[Para [Str "koodi",Space,Code ("",[],[]) "f(x) = x"]]

Code nodessa näyttäisi oleva sama Attr, eli esimerkiksi seuraava on validia AST-syntaksia (koodipätkän tunniste “koodi1”, ohjelmointikieli “julia”, lisäksi key-value pari “evaluate” = “false”):

[Para [Str "koodi",Space,Code ("koodi1",["julia"],[("evaluate", "false")]) "f(x) = x"]]

Pandocin Markdown-formaatissa tämä olisi

koodi `f(x) = x`{#koodi1 .julia evaluate="false"}

Tämä on kuitenkin sellaista formaattia, ettei sitä markdown-parserit ihan kakistelematta niele. Esimerkiksi GitHub ei näitä ylimääräisiä parametreja tunnista mitenkään. Se on kuitenkin mielenkiintoinen laajennos ja huomionarvoista on, että mikäli markdownista haluaa LaTeXiin mennä, niin jonnekin on pakko laittaa ylimääräinen metatieto jota tarvitaan kun hienosäädetään renderöintiä. Esimerkiksi markdown ei tue millään tavoin otsikon laittamista koodiblokkiin saatikka sen sisällyttämistä esimerkiksi kelluvaan listing ympäristöön. Annetun metadatan perusteella voisin esimerkiksi viitata koodiin sen id-numeron perusteella tai suorittaa koodin ennen sen renderöimistä. Nämä ovat varsin mielenkiintoisia ajatuksia ja palaan näihin kun tutkimukseni Pandocin kanssa etenevät.

Kuvat (Image)

Tarkastellaan seuraavaksi kuvien ominaisuuksia. LaTeXissa kuvan latominen menisi tyypillisesti näin.

\begin{figure}
\centering
\includegraphics{hello.png}
\caption{Kuvan otsikko}\label{fig:kuva}
\end{figure}

Tässä näkyy myös konkreettisesti, mikä Markdownissa on pielessä. Kuviin, kaavoihin ja muihin vastaaviin pitäisi viitata niiden tunnisteen perusteella, mutta koska Markdownissa ei viittausjärjestelmä ole niin pitkälle kehittynyt kuin LaTeXissa, sen sijaan että kirjoitettaisiin “LaTeXissa kuvan latominen menisi listauksen 1 mukaisesti”, käytetään kaikenlaisia muita ilmaisuja kuten “alla olevan mukaisesti”, “seuraavalla tavalla”, ja niin edelleen.

Mikäli kuvaa tällaisenaan yrittää tarjota Markdown-formaatissa, menisi se Pandocin AST:hen RawBlock-tyyppisenä:

[RawBlock (Format "latex") "\\begin{figure}\n\\centering\n\\includegraphics{hello.png}\n\\label{fig:kuva}\\caption{Kuvan otsikko}\n\\end{figure}"]

Tämä RawBlock varmaankin renderöityy LaTeXissa ihan hyvin ja sellaisenaan. Ongelmaksi tulee ettei se tarkoita Markdownissa yhtään mitään. Myöskään LaTeX-formaatista tarjoiltuna AST ei näytä kovin hyvältä:

[RawBlock (Format "latex") "\\centering"
,Para [Image ("",[],[]) [Str "Kuvan",Space,Str "otsikko"] ("hello.png","fig:"),SoftBreak,Span ("fig:kuva",[],[("label","fig:kuva")]) [Str "[fig:kuva]"]]]

Jos Markdownista tarjoaa kuvan seuraavalla tavalla:

![kuvateksti](kuva.png)

Saadaan

[Para [Image ("",[],[]) [Str "kuvateksti"] ("kuva.png","fig:")]]

Lämpenee. Dokumentaation perusteella tämä rakenne on hieman samantyyppinen kuin Code.

Image Attr [Inline] Target
Image: alt text (list of inlines), target

Ensimmäisessä Attr-osiossa on siis yksilöivä id, luokat ja key-value parit, sen jälkeen tulee Inline-blokki joka voidaan käyttää esimerkiksi kuvatekstiin, ja lopuksi tulee Target joka on muotoa (url, title). Eli jos tämän natiiviformaatissa mankeloi niin että jokaisessa kentässä on jotakin, niin saadaan.

[Para [Image ("kuva1",["kissakuvat"],[("show", "true")]) [Str "kuvateksti"] ("kuva.png","fig:kuvatitle")]]

Nyt kuva LaTeX-formaatissa on

\begin{figure}
\hypertarget{kuva1}{
\centering
\includegraphics{kuva.png}
\caption{kuvateksti}\label{kuva1}
}
\end{figure}

Kaikkea dataa ei käytetä kaikkien kirjoittajien toimesta. Key-value pari :show => true on vain täytteenä. Periaatteessa pandocin LaTeX-kirjoittajassa voi olla erilaisia optioita joilla voi hienosäätää, miltä kuva näyttää. Esimerkiksi tässä tilanteessa puuttuu kontrolli siitä, käytetäänkö \centering-komentoa kuvan keskittämiseen vai ei. Pandocin LaTeX-kirjoittaja lisää sen automaattisesti. Mutta hienosäätöä varten meillä pitäisi olla Image-noden parametrilistassa sopivia asetuksia, joita LaTeX-kirjoittaja sitten voisi käyttää hyödykseen. centering: true olisi eräs varsin hyvä esimerkki tällaisesta.

Target on käytännössä sama kuin HTML-kielen hyperlinkki, ja näyttäisi että ainakaan LaTeX-kirjoittaja ei käytä tuota title-tietoa mihinkään.

Markdownissa tämä Image-node on

![kuvateksti](kuva.png 'kuvatitle'){#kuva1 .kissakuvat show="true"}

HTML-kielellä

<figure>
  <img
    src="kuva.png"
    title="kuvatitle"
    alt="kuvateksti"
    id="kuva1"
    class="kissakuvat"
    data-show="true"
  />
  <figcaption>kuvateksti</figcaption>
</figure>

Pandocin AST:ssä Image on inline-elementti eikä block-elementti, kuten CodeBlock.

Yhtälöt

Yhtälöiden latominen on eräs LaTeXin tärkeimpiä, jos ei tärkein ominaisuus. Tarkastellaan, mitä tapoja Pandocin AST tarjoaa. LaTeXissa yhtälön ladonta tekstin sekaan menee suurinpiirtein näin:

Yhtälössä
\begin{equation}
f(x) = y \label{eq:kaava}
\end{equation}
ei ole virhettä.

Jos yhtälö kirjoitetaan tuolla tavalla Markdowniin, esitystapa Pandocin AST:ssä on

[Para [Str "Yhtälössä",SoftBreak,RawInline (Format "tex") "\\begin{equation}\nf(x) = y \\label{eq:kaava}\n\\end{equation}",SoftBreak,Str "ei",Space,Str "ole",Space,Str "virhett\228."]]

LaTeXista parsittuna se on

[Para [Str "Yhtälössä",SoftBreak,Math DisplayMath "f(x) = y \\label{eq:kaava}",SoftBreak,Str "ei",Space,Str "ole",Space,Str "virhett\228."]]

LaTeXilla inline-yhtälö kirjoitetaan $-merkeillä:

Yhtälö $f(x) = x^2$ on hieno yhtälö.

Pandocin AST:hen se parsiutuu:

[Para [Str "Yhtälö",Space,Math InlineMath "f(x) = x^2",Space,Str "on",Space,Str "hieno",Space,Str "yhtälö."]]

Meillä on siis AST:ssä node

Math MathType Text

Jossa MathType on joko InlineMath tai DisplayMath. Markdownissa DisplayMath blokki tehdään $$-syntaksilla ja InlineMath sitten $-syntaksilla.

Joissakin Markdown-implementaatioissa olen nähnyt että yhtälöt kirjoitetaan backtickeillä:

```math
f(x) = x^2
```

Pandocin AST:ssä tämä tallentuu CodeBlock-nodeen:

[CodeBlock ("",["math"],[]) "f(x) = x^2"]

Yhtälöön viittaus \eqref-komennolla, eli

Katso yhtälöä \eqref{eq:testi}.

kääntyy LaTeX-formaattiin sellaisenaan, ja näyttäytyy Pandocin AST:ssä RawInline-lohkona.

Teknisesti, jos tarkoitus on ainoastaan tehdä LaTeX-dokumenttia Markdownilla, niin olisi perusteltua käyttää LaTeX-ympäristöjä Markdownin seassa. Mutta silloin ongelmaksi kyllä muodostuu, ettei sillä tavalla tehtyä RawBlock:ia voi hyödyntää missään muussa formaatissa.

Meillä täytyy olla järkeviä tapoja viitata kaavoihin. Eräs työkalu ei vain kaavoihin viittaukseen, vaan ristiviittauksiin yleisesti ottaen on [pandoc-crossref][pandoc-cross-ref].

Matematiikan ladonta tulee todennäköisesti olemaan kaikista konstikkainta, sillä matemattisia rakenteita on hyvinkin monimutkaisia ja pitäisi olla mahdollista esimerkiksi viitata yhtälöryhmän yksittäiseen yhtälöön jotenkin luontevasti. HTML-sivuille voi matematiikkaa latoa ainakin MathJaXin ja KaTeXin avulla, mutta niiden ominaisuudet eivät ole lähimaillakaan mitä LaTeXissa on totuttu näkemään.

Toisin sanoen, ei kannata edes odottaa, että matematiikka internetissä olevassa hypertekstidokumentissa, mikä Markdownistakin lopulta generoidaan, olisi niin laadukasta kuin mitä se LaTeX-dokumentissa on. Tavoiteltavaa kuitenkin on, että Markdownissa voisi kirjoittaa yhtälön sillä tavalla, että kaikki tarpeellinen metadata voidaan viedä LaTeX-dokumenttiin jossa sitä sitten hyödynnetään.

HTML

Hei, maailma!

<div>
    <p>Tässä sitä nyt on, <b>HTML-koodia</b></p>
</div>

Tämä on natiiviformaatissa

[Para [Str "Hei,",Space,Str "maailma!"]
,Div ("",[],[])
 [CodeBlock ("",[],[]) "<p>T\228ss\228 sit\228 nyt on, <b>HTML-koodia</b></p>"]]

Pandocissa itse asiassa on sekä Div että Span ympäristöt, sekä tietenkin Para ja Strong, joten tämä kyseinen kokeilu pitäisi olla täysin kuvattavissa AST:ssä.

Kun käännetään natiiviformaatista takaisin HTML-koodiksi, lopputulos ei ole kovinkaan hyvä:

<p>Hei, maailma!</p>
<div>
<pre><code>&lt;p&gt;Tässä sitä nyt on, &lt;b&gt;HTML-koodia&lt;/b&gt;&lt;/p&gt;</code></pre>
</div>

Markdownista

Hei, maailma!

::: {#paragraph1 .stuff text_quality="crap"}
Tassa sita nyt on, *HTML-koodia*.
:::

Tässä Div-ympäristö saadaan kolmella tai useammalla kaksoispisteellä. Tulos natiivissa formaatissa on:

[Para [Str "Hei,",Space,Str "maailma!"]
,Div ("paragraph1",["stuff"],[("text_quality","crap")])
 [Para [Str "Tassa",Space,Str "sita",Space,Str "nyt",Space,Str "on,",Space,Emph [Str "HTML-koodia"],Str "."]]]

Tämä edelleen HTML-formaatissa on

<p>Hei, maailma!</p>
<div id="paragraph1" class="stuff" data-text_quality="crap">
<p>Tassa sita nyt on, <em>HTML-koodia</em>.</p>
</div>

Tämä kääntyy natiiviksi täysin hyvin. On myöskin mahdollista saada tästä RawBlock- ja RawInline-esityksiä formaatilla html:

[RawBlock (Format "html") "<p>"
,Plain [Str "Hei,",Space,Str "maailma!"]
,RawBlock (Format "html") "</p>"
,Div ("paragraph1",["stuff"],[("data-text_quality","crap")])
 [RawBlock (Format "html") "<p>"
 ,Plain [Str "Tassa",Space,Str "sita",Space,Str "nyt",Space,Str "on,",Space,RawInline (Format "html") "<em>",Str "HTML-koodia",RawInline (Format "html") "</em>",Str "."]
 ,RawBlock (Format "html") "</p>"]]

Yhteenveto

Tässä ei suinkaan ollut kaikki mahdolliset nodet mitä Pandocin AST tarjoaa. Siellä näyttäisi olevan muita tärkeitä nodeja kuten Meta, joka tallentaa Markdown-dokumentin alussa olevat muuttujat. Sitten on Para, BlockQuote, OrderedList, BulletedList, Header, Table ynnä muuta sellaisia lohkoelementtejä. Inline-elementtejä on sitten Str, Emph, Strong, Cite, Link, Note, Span ja niin edelleen.

Dokumenttien onnistuneessa konversiossa kyse on lähinnä kolmesta asiasta. Ensinnäkin, onko Pandocin AST:ssä kaikki tarpeelliset rakenteet dokumenttien onnistuneeseen kuvaamiseen? Vastaisin että kyllä, sillä viimekädessä Pandociin voidaan implementoida puuttuvia rakenteita, tai käyttää formaattispesifejä RawInline ja RawBlock elementtejä.

Seuraava kysymys on, että mikä tiedostoformaatti tukee näitä kaikkia rakenteita jotta Pandoc voisi sen perusteella parsia dokumentin täydellisesti omaan abstraktiin syntaksipuuhun? Todennäköisesti XML on tätä tavoitetta melko lähellä, mutta se ei ole formaattina sellaista, jota mielellään kirjoittaa. Markdown on nykyään hyvin yleinen formaatti sen yksinkertaisuuden vuoksi, mutta kuten tässäkin nähtiin, esimerkiksi matemaattisten kaavojen ladonta voi tuottaa päänvaivaa.

Ja kolmas kysymys on, että mitenkä tämä AST sitten kirjoitetaan jollekin toiselle kielelle? Tämä ei varmastikaan ole todellinen ongelma, sillä AST:n kirjoittaminen mihin tahansa formaattiin on triviaali, joskin työläs, toimenpide. Kaikki formaatit tukevat jonkinlaista kommentointimahdollisuutta, jolla ylimääräistä metadataa, jota ei dokumentissa voida kuitenkaan hyödyntää, saadaan tallennettua myös toiseen formaattiin.

Todella monimutkaisissa osuuksissa voimme varmaankin ainakin edetä niin, että osuus kirjoitetaan RawBlock-noden avulla useilla eri formaateilla, ja sitten käytetään sopivaa filtteriä joka poistaa osan nodeista riippuen mihin formaattiin AST:tä ollaan kirjoittamassa. Tällä tavalla voimme myös varioida dokumenttia, sillä PDF ei tarvitse kaikkea sitä tietoa, mitä yleisesti ottaen hypertekstidokumentissa esiintyy, ja toisaalta myös toisin päin on vastaava: hypertekstidokumenttiin ei tarvita kaikkia niitä osioita, jotka luontevasti esiintyvät PDF-dokumentissa.