momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Pigmentarea secvențelor de cod din documente LaTeX (I)

LaTex | memoizare
2023 jun

Pachetul LaTeX pygjust

Referindu-ne la stilarea programelor sursă întâlnite într-un fișier LaTeX dat, în [1] am constituit pachetul minimal pygjust.sty în care implicam Pygments (într-un mediu bazat pe fancyvrb), cum se procedează și în pachetele larg cunoscute minted și verbments – dar spre deosebire, aveam ca opțiune doar specificarea limbajului de evidențiat sintactic.
Subliniem că (ținând iarăși de „minimal”) vizăm numai formaterea LaTeX.

Rescriem totuși pygjust.sty, adăugând posibilitatea de numerotare a liniilor secvenței de cod – ceea ce nu-i la îndemână „dinafară” și poate fi necesară în contextul în care apare secvența respectivă. Deasemenea, prevedem ca fișierele LaTeX create de pygmentize (pentru a formata secvența curentă), să fie salvate ca fișiere distincte – în [1] ne foloseam de un același fișier pentru toate secvențele de cod, încât numai pentru ultima, puteam dispune de textul comenzilor de marcare, în scopul unor eventuale ajustări manuale.

Definim și setăm opțiuni "cheie=valoare" (pentru limbaj, mărimea fontului și numerotare de linii), toate cu valori implicite potrivite – folosind acum și pachetul xkeyval. Prevedem ca secvența care trebuie pigmentată să fie ambalată sub \begin{Pyg} – definind mediul Pyg pe baza unor medii Verbatim din pachetul fancyvrb. Pentru a constitui nume distincte fișierelor pigmentoare, angajăm un contor asupra mediilor Pyg întâlnite consecutiv în documentul LaTeX în care vom fi încărcat pygjust.sty:

\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{pygjust}[pygmentize() the programs within a LaTeX file]

% The same Pygments-style ("default" for example) applies to all programs:
%     pygmentize -S default -f latex > Default.sty    # https://pygments.org/
% Programs must be contained in Pyg environments (based on Verbatim)

\RequirePackage{Default, xkeyval, fancyvrb, xcolor}

\define@cmdkey{Pyg}[py@]{lang}[text]{}                     % lang=text 
\define@key{Pyg}{fontsize}[\small]{\fvset{fontsize = #1}}  % fontsize=\small
\define@boolkey{Pyg}[py@]{nrcrt}[false]{}                  % nrcrt=false

\newcommand\pyset[1]{\setkeys{Pyg}{#1}}  % sets default option values

\newcounter{Pyg}  % to identify code sections and related files
\setcounter{Pyg}{1}

\newcommand\pygrun{pygmentize 
     -P encoding=utf-8 
     -l \py@lang\space 
     -O escapeinside='ßß'  % allow LaTeX commands (not inside comments)
     -P texcomments  % allow LaTeX commands inside comments
     -P mathescape  % allow math mode inside a comment ('$...$')
     \ifpy@nrcrt -P verboptions='numbers=left, numbersep=4pt, 
                                 numberblanklines=false'\space\fi  % numbering
     -o \thePyg.tex \jobname.lst}  % write TeX file of the pigmentized code

\newenvironment{Pyg}[1]{%    \begin{Pyg}{<options>} <source code> \end{Pyg}
    \pyset{#1}  % sets the values of the indicated options
    \VerbatimEnvironment
     \begin{VerbatimOut}{\jobname.lst}}  % save the code sequence
    {\end{VerbatimOut}
     \immediate\write18{\pygrun}  % launch the external program pygmentize
     \input{\thePyg.tex}  % replace the code sequence with pigment code
     \stepcounter{Pyg}}

\pyset{lang, fontsize, nrcrt}  % reset to default values

\endinput

Opțiunea 'lang' este definită drept "cmdkey" – producând macro-ul \py@lang care va reține valoarea de furnizat parametrului "-l" al programului pygmentize (fie valoarea implicită "text", fie valoarea găsită în \begin{Pyg}{cheie=valoare,...}).

Pentru „mărimea fontului” am folosit cheia (foarte comună, de altfel) 'fontsize' – existentă și în pachetul fancyvrb, încât am putut invoca macro-ul acestuia \fvset, pentru a seta ca valoare implicită \small (că de obicei, secvențele de cod se redau ceva „mai mic”, decât textul de bază al documentului).

Prin \fvset puteam introduce și opțiunile numbers (cu valorile posibile none/left/right) și numbersep (distanța la care este scris numărul, față de linii); dar important este dacă utilizatorul consideră că trebuie să numeroteze liniile secvenței de cod – dacă da, atunci poziția firească a numerelor este 'left', la cam 4pt (4 „puncte tipografice”) în afara liniei (a-i oferi cele două opțiuni cam seamănă cu a-l forța să meargă pe rând la două ghișee, deși nu este neapărat necesar).
Așa că, în loc de a prevedea cele două chei, am definit macro-ul '\py@nrcrt', în care am salvat valoarea implicită 'false' și apoi, am ținut cont de faptul că pygmentize pasează lui \fvset, acele chei (prevăzute în fancyvbr) pe care le primește în parametrul 'verboptions'.
Comanda \ifpy@nrcrt (creată de xkeyval) testează valoarea curentă din \py@nrcrt; dacă este true, atunci pygmentize va primi prin verboptions cele două setări (pentru 'numbers' și 'numbersep') și le va pasa lui \fvset; cel mai firesc este să numerotăm numai liniile de cod, nu și pe cele albe – încât am pasat și opțiunea numberblanklines (setând 'false').

Pentru a seta opțiuni dintre cele trei prevăzute, am introdus macro-ul \pyset; acesta invocă \setkeys (din pachetul xkeyval), beneficiind de faptul că toate cheile au fost încadrate într-o aceeași „familie” (pe care am numit-o "Pyg").

Compilând fișierul articol.tex de exemplu, în care am folosi pygjust – vor rezulta și fișierele "1.tex", "2.tex" etc., conținând fragmentele LaTeX prin care s-au pigmentat secvențele de cod întâlnite în document (încât autorul documentului are posibilitatea de a face eventuale ajustări, pentru una sau alta dintre acestea).
N-a fost necesar să folosim opțiunea -f a lui pygmentize, pentru a-i fi precizat ce formator (HTML, LaTeX, etc.) să folosească pentru pigmentare – văzând extensia .tex, va folosi formatorul pentru LaTeX (iar pentru .html l-ar folosi automat pe cel pentru HTML).
Precizăm că prin pygmentize --help putem vedea ce opțiuni (-l, -f, -P, etc.) sunt disponibile.

Desigur, dacă vom compila într-un același director, două sau mai multe articole .tex – atunci fișierele "1.tex", "2.tex" etc. vor corespunde ultimului compilat… Însă, „cele mai bune practici” ne spun să lucrăm în directoare distincte, articolele respective (și am renunțat fără nicio grijă, la nume de fișier în care să fi adăugat și \jobname, precum \thePyg\jobname.tex).

Clarificare principială a funcționării

Să presupunem că lucrăm folosind LaTeX, la un articol de programare și vrem să evidențiem sintactic (poate și dintr-un alt punct de vedere) secvențele de cod existente…

Pentru a folosi pachetul creat mai sus, adăugăm în preambulul documentului:

\usepackage{pygjust}

Ambalăm fiecare secvență de cod din corpul documentului după această schemă:

\begin{Pyg}{lang=python}
    %% secvență de cod Python %%
\end{Pyg}

Compilând (prin xelatex) fișierul LaTeX rezultat astfel, obținem fișierul PDF corespunzător (pe care îl putem vizualiza folosind de exemplu, un browser – Firefox).

Întâlnind \begin{Pyg}, compilatorul va căuta definiția mediului "Pyg", o va depista în pachetul pygjust și o va „instanția” imediat: mai întâi va executa comanda \pyset{#1} – prin care în cazul exemplificat mai sus, valoarea "python" care a fost explicitată pentru cheia lang, va fi „salvată” în macroul \py@lang (celelalte chei rămân pe valorile implicite ale lor).

Apoi, se va crea un mediu VerbatimOut, prin care secvența de cod Python – și oricare altă secvență de cod întâlnită ulterior – va fi scrisă într-un (același) fișier denumit după \jobname (macrou care păstrează numele documentului compilat), cu „extensia” .lst.

Apoi, prin \immediate\write18{\pygrun} se lansează în execuție programul extern pygmentize, precizându-i să citească din fișierul ".lst" curent și să scrie rezultatul pigmentat în fișierul "\thePyg.tex" (care este un fragment LaTeX). Desigur, compilatorul trebuie încredințat că are voie să scrie pe disc – lansându-l cu opțiunea de compilare -shell-escape.

În final, prin \input{}, conținutul fișierului "\thePyg.tex" este introdus în document peste secțiunea de cod inițială ("\begin{Pyg}" devine "\begin{Verbatim}"); contorul de medii "Pyg" este incrementat, în vederea abordării la fel a următoarei secvențe de cod din document.

Exemplificare

Să scriem, un „articol de programare” – dar nu vrem decât să ilustrăm și să completăm cele de mai sus, încât ne vom rezuma la a compune câteva secvențe de cod disparate.

Creem fișierul articol.tex (tastând pe linia de comandă touch articol.tex), îl deschidem în editorul de text gedit și începem să scriem, folosind LaTeX:

\documentclass[a4paper, 12pt]{report}
    %% preambul %%
\begin{document}
    %% corpul sau conținutul documentului %%
\end{document}

(bineînțeles că lăsăm spații verticale mai largi decât am redat aici, pentru "preambul" și "corp" …)

De obicei, secvențele de cod sunt redate cu o mărime de caracter mai mică decât cea de bază; alegând ca mărime de bază 12pt (dar nu mai mică de 11pt) ne-am asigurat că secvențele de cod vor fi suficient de lizibile, chiar și când vom folosi \footnotesize (cum se obișnuiește pentru porțiunile de comentariu, specifice secvențelor de cod).

Preambulul documentului

De obicei, în preambul se specifică în primul rând fontul de bază în care vrem să fie redat documentul, precum și fontul de redare a porțiunilor de cod; intenționăm să compilăm prin xelatex, încât putem angaja pachetul fontspec, din care folosim comenzile \setmainfont și \setmonofont, alegând familiile de fonturi TeX Gyre Pagella și DejaVu Sans Mono:

\usepackage{fontspec}
\setmainfont{texgyrepagella-regular.otf}[
    BoldFont = texgyrepagella-bold.otf,
    ItalicFont = texgyrepagella-italic.otf,
    BoldItalicFont = texgyrepagella-bolditalic.otf,
    Scale=MatchUppercase,
    Mapping=tex-text, Language = Romanian 
]
\setmonofont{DejaVuSansMono.ttf}[
    Scale=MatchLowercase,
    BoldFont = DejaVuSansMono-Bold.ttf,
    ItalicFont = DejaVuSansMono-Oblique.ttf,
    BoldItalicFont = DejaVuSansMono-BoldOblique.ttf
]

Unele cuvinte-cheie din secvențele de cod vor fi redate „boldat”, iar comentariile vor fi redate „italic”; de aceea este important să prevedem toate variantele ("BoldFont", "ItalicFont", etc.) – altfel, compilatorul va căuta să substituie cumva, varianta omisă.

Pentru fontul de bază ales, am specificat "Language=Romanian" – încât fontspec va asigura alinierea și spațierea cea mai potrivită, literelor „cu diacritice” specifice limbii române.
Dacă vrem, putem avea în cursul compilării și despărțire automată în silabe (corectă cam în 98% din cazuri, pentru limba română), adăugând în preambul:

\usepackage{polyglossia}
\setmainlanguage{romanian}

Nu ne rămâne, în cazul de față, decât să specificăm în preambul pachetul prin care să marcăm sintactic secvențele de cod (bineînțeles… alegem pygjust):

\usepackage{pygjust}

Este important să compilăm din când în când, pentru a vedea dacă până la momentul respectiv nu avem cumva vreo eroare – tastând pe linia de comandă a unui terminal:

xelatex articol.tex  # No pages of output. Transcript written on articol.log.

Putem deschide într-un "Document Viewer" (de exemplu, evince) fișierul PDF rezultat, pentru a verifica prin meniul "Properties" că au fost încorporate fonturile pe care le specificasem.

Mai departe, începem să încropim conținutul documentului articol.tex

Corpul documentului

Am deschis în Firefox un articol scris mai demult, folosind HTML: Calculul coeficienţilor binomiali. Vedem acolo mai multe secțiuni de cod, iar acestea sunt deja pigmentate (fiindcă înainte de a le fi încorporat în fișierul HTML respectiv, le-am trecut prin programul pygmentize, cu -f html); dacă selectăm una cu mouse-ul și o copiem în "clipboard" (tastând Ctrl+C) – vom obține textul propriu-zis al secvenței respective (ne-pigmentat).
Am selectat și am decupat de acolo, pe rând, două secvențe de cod și le-am pastat în corpul documentului articol.tex (pe care ne apucasem să-l edităm în gedit); apoi, le-am ambalat în câte un mediu \begin{Pyg}, precizând unele dintre opțiunile oferite de pachetul pygjust.

Fiindcă pygmentize va fi lansat (prin macro-ul \pygrun din pygjust) cu parametrul texcomments activat – în interiorul comentariilor existente în secțiunea de cod putem folosi comenzi LaTeX; înscriem la începutul comentariilor comanda \footnotesize (pentru a le reda cu o mărime de caracter mai mică) și în interiorul acestora, ambalăm unii termeni în \emph (prin care se va comuta între "italic" și "normal", stilul de redare a acelui termen).

Fiindcă pentru pygmentize am prevăzut și parametrul escapeinside='ßß', comenzile LaTeX vor fi recunoscute (cu unele excepții, v. [1]) în interiorul unei linii de cod – dacă ambalăm între caractere 'ß' porțiunea pe care vrem să o marcăm; de exemplu, pentru a evidenția nu atât sintaxa, dar și logica lucrurilor – putem înlocui '+' care apare între două apeluri de funcții, prin ß\textbf{+}ß (astfel, '+' va fi redat nu ca un "operator" obișnuit, ci „boldat”).
Obs. Pe sistemul nostru, caracterul 'ß' este produs prin combinația de două taste altgr + W.

Pentru a reda aici, conținutul rezultat în articol.tex – l-am pigmentat folosind lexer-ul pentru LaTeX (din Pygments); subliniem că '$' apare „pigmentizat” nu fiindcă ar ține de sintaxa limbajului în care este formulată secvența de cod, ci fiindcă în LaTeX caracterul '$' este „special” (delimitează expresii matematice, redate apoi cu un „font matematic”):

\begin{document}
\thispagestyle{empty}  % omite numărul de pagină
        Un program în limbajul \textsf{Perl}:
\begin{Pyg}{lang=perl, nrcrt=true}
#!/usr/bin/perl -w
use Memoize;  #\footnotesize see \emph{https://perldoc.perl.org/Memoize}
memoize('comb');

sub comb {
    my $n = shift; my $k = shift;
    return $n if $k == 1;
    return 1 if $k == $n;
    comb($n-1, $k-1) ß\textbf{+}ß comb($n-1, $k);
}  #\footnotesize returnează numărul combinărilor de \emph{n} câte \emph{k}

print "C(28, 14) = ", comb(28, 14);  #\footnotesize \emph{40116600}
\end{Pyg}

        Un program în limbajul \textsf{Python}:
\begin{Pyg}{lang=python}

class Memoize: #\footnotesize "decorează" o funcţie cu codul de memoizare
    def __init__(self, function):
        self.fn = function
        self.cache = {}
    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.fn(*args)
        return self.cache[args]

@Memoize  #\footnotesize ambalează funcţia următoare într-un obiect \emph{Memoize}
def comb(n, k):
    if k == 1:
        return n
    if n == k:
        return 1
    return comb(n-1, k-1) ß\textbf{+}ß comb(n-1, k)

print("C(142, 71) = ", comb(142, 71))  
# \emph{372641034574519600278163693967644731577200}
\end{Pyg}

\end{document}  % încheie articol.tex

Precizăm că înainte de a înscrie în articol.tex cele două secvențe de cod, le-am rulat pe fiecare în parte – pentru a ne încredința că sunt încă valabile (fiindcă articolul din care le-am decupat, menționat mai sus, a fost scris acum mai bine de 10 ani); dat fiind că acum folosim python3, în programul Python (probabil, "2.7") preluat din articolul respectiv, a fost necesar să înlocuim "if not self.cache.has_key(args)" prin "if args not in self.cache".

Rezultatul compilării documentului; ajustări

Lansăm compilatorul xelatex, de pe linia de comandă (folosind opțiunea "-shell-escape"):

xelatex -shell-escape  articol.tex

Nu includem aici, fișierul articol.pdf rezultat astfel, ci redăm doar o imagine PNG decupată din fereastra Firefox în care l-am deschis:

Liniile programului apar numerotate – conform opțiunii nrcrt=true pe care o montasem pe mediul \begin{Pyg} în care ambalasem secvența de cod Perl (și apar fără numere de linii, în cazul programului Python, căruia nu i-am prevăzut această opțiune).
Comentariile din program – pe care lexer-ul pentru Perl din Pygments le-a recunoscut după prefixul '#' – sunt stilate "italic", exceptând porțiunile pe care le-am ambalat în \emph; comentariile din liniile 1 și 10 sunt redate cu mărimea de caracter corespunzătoare valorii implicite (\small) a opțiunii "fontsize", iar cele din liniile 2 și 9 sunt redate ținând seama că aceste linii fuseseră „prefixate” cu \footnotesize.
În loc să apară ca și operatorii obișnuiți ('=' din linia 5, sau '==' din linia 6), semnul '+' din linia 8 apare „boldat” (și "black") – conform ajustării pe care i-o aplicasem direct (folosind parametrul escapeinside).

În urma compilării, ne-au rezultat și fișierele ".tex" emise din mediile \begin{Pyg} corespunzătoare celor două secvențele de cod.
Decupăm aici o linie mai scurtă, din "1.tex" – pe cea aferentă liniei numerotate cu 4 în imaginea de mai sus (dar o redăm după ce am trecut-o prin pygmentize):

\PY{k}{sub} \PY{n+nf}{comb} \PY{p}{\PYZob{}}

Lexer-erele din Pygments separă în "tokeni" conținutul secvenței de cod și le asociază câte o mnemonică scurtă: "k" desemnează „cuvintele-cheie” din limbajul respectiv (precum sub, return, if în Perl); "n" (și "nv", "nf" etc.) desemnează nume de funcții sau de variabile; "c" (și "c1", etc.) desemnează categorii de comentariu; ș.a.m.d.
Default.sty – preluat în pygjust din Pygments – definește macro-urile prin care fiecărei categorii de "tokeni" i se asociază câte un anumit stil de redare; construcția acestor macro-uri nu este ușor de înțeles și nu ne străduim aici să o lămurim – dar redăm totuși o linie din Default.sty, în care se vizează tokenii "nume de funcție" (clasați ca "nf"):

\@namedef{PY@tok@nf}{\def\PY@tc##1{\textcolor[rgb]{0.00,0.00,1.00}{##1}}}

Deducem de aici că termenii de clasă "nf" vor fi redați în culoarea "blue" (pentru care componentele de culoare "Red, Green, Blue" sunt 0, 0, 1).
În linia numerotată cu 4 se introduce subrutina comb, iar numele respectiv este stilat prin \Py{n+nf} – încât la redare este colorat "blue". Dar să observăm această nuanță: apelurile subrutinei – v. liniile 8 și 10 – sunt pigmentate cu \Py{n} (fără "nf").

Poate că am prefera să stilăm altfel, de exemplu: să introducem funcțiile „definite de utilizator” în stilul "black-bold", iar apelurile de funcții în stilul "blue":

Pentru a reda astfel linia 4, am revenit în articol.tex și am înscris în locul respectiv ß\textbf{comb}ß; iar pentru a reda apelurile din liniile 8 și 10 – am definit în cadrul documentului o comandă \pygb cu două argumente:

\newcommand\pygb[2][\textbf]{#1{\PY{n+nf}{#2}}}

Argumentul #1 este opțional și are ca valoare implicită macroul \textbf; iar #2 este termenul de pigmentat (obligatoriu).
Putem invoca \pygb numai cu argumentul obligatoriu – sau cu valoare „vidă” pentru argumentul opțional, sau precizând acestuia o anumită valoare (de exemplu, \textit):

Se subînțelege că în articol.tex am adăugat programului Perl liniile numerotate în imagine cu 11..13; pe fiecare linie am folosit întâi '|' în loc de 'ß', pentru a reda textul comenzii de pigmentare și apoi am revenit la 'ß', pentru a reda alăturat și efectul pigmentării.
(bineînțeles că am recompilat și din noul articol.pdf, am decupat porțiunea redată mai sus)

vezi Cărţile mele (de programare)

docerpro | Prev | Next