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

Pigmentizarea programelor sursă în LaTeX

LaTex | R
2022 apr

Scop şi… zorzoane, în acelaşi pachet

Să zicem că ai de scris un articol, vizând chestiuni de programare; dacă intenţionezi să postezi pe un blog, foloseşti HTML; dacă vrei să îndrepţi articolul către o anumită revistă, sau dacă este vorba de o carte proprie, ai de folosit LaTeX; dacă uneori, merită să transferi dintr-o parte în alta – converteşti prin Pandoc.
Fiind vorba de programare, vei avea de inserat şi unele texte-sursă de program (bineînţeles că nu este vorba de programe banale – altfel nu ai avea de ce să te apuci de scris). Mai departe vizăm articolul/cartea prin A şi un program-sursă prin S.

Este simplu să inserezi direct, S: dacă scrii A folosind HTML, atunci înfiinţezi în locul dorit un element <pre class="P-style"> şi pastezi dedesubt textul S; dacă scrii A folosind LaTeX, atunci înfiinţezi un element (un environment) verbatim în care pastezi S.
Este uşor apoi, să şi marchezi anumite cuvinte-cheie din textul pastat în <pre> – de exemplu, ambalând "for" într-un element <b>, sau ambalând fiecare comentariu din S în <em>; analog, în LaTeX putem folosi pachetul fancyvrb care redefineşte "verbatim" pentru a permite şi ambalarea unor porţiuni de text în comenzi de formatare, de exemplu \textbf{for} sau \emph{\small{# Comentariu din S}}).

Dar simplitatea inserării directe este înşelătoare – fiindcă ai nu un singur S de inserat, ci mai multe. De obicei se apelează la un „pigmentizator”, care primind fişierul-sursă S îţi returnează un fragment HTML (sau LaTeX, după caz) în care deja, termenii din S (cuvinte-cheie, comentarii, punctuaţie, etc.) sunt marcaţi după sintaxa specifică limbajului în care este formulat S; un pigmentizator mult folosit este programul pygmentize, care exploatează pachetul Python Pygments.

Aaa… şi aşa, te loveşti de faptul că ai de inserat nu un singur S: trebuie să apelezi pygmentize pentru fiecare S şi să pui rezultatul, de fiecare dată, în locul cuvenit din A.
De fapt, nu este chiar aşa… La CTAN găsim pachete care asigură automatizarea lucrurilor: incluzând de exemplu minted (sau pachetul mai vechi, verbments) – în cursul compilării prin care obţii formatul PDF pentru fişierul LaTeX în care ai scris A, pygmentize va fi invocat pentru fiecare S depistat în text şi rezultatul va fi inserat în mod automat, în locul lui S.
Şi chiar dacă nu am avea vreun asemenea pachet (nu ştim de aşa ceva, pentru cazul când A este scris în HTML, nu în LaTeX) – tot n-ar fi cine ştie ce, de formulat un mic program prin care să se identifice secţiunile S, să se paseze lui pygmentize conţinutul respectiv şi să se înscrie înapoi rezultatul.

Dar toate aceste pachete, cam exagerează (încălcând până la urmă un principiu binecunoscut şi clamat frecvent: „fă un singur lucru şi fă-l bine”): îşi propun să acopere toate necesităţile potenţiale, imaginând o groază (mai mare sau mai mică) de „facilităţi” – între care cam multe sunt din categoria „zorzoane” şi ar putea fi satisfăcute (dacă ar fi cazul) şi fără intermedierea pachetului respectiv.

Scopul principal ar fi acela de a asigura invocarea automată a programului pygmentize, pentru a marca sintactic toate secţiunile S din A; trebuie referite desigur, opţiunile programului pygmentize – dar numai acestea – dintre care de fapt, una singură este într-adevăr necesară, ca parametru: limbajul în care este formulat S (alte opţiuni importante este bine să fie activate, chiar dacă nu le folosim în toate secvenţele S).
Etichetarea sau titrarea lui S (pentru care pachetul oferă opţiuni de aşezare, de aliniere, de dimensionare, etc.) poate fi necesară, pentru a putea referi S dintr-un alt loc din A – dar pentru a eticheta S (fie şi cu un "caption") nu prea avem nevoie să folosim opţiunile speciale oferite de pachetul respectiv.
Bordarea conţinutului lui S (folosind parametrii oferiţi, pentru grosime, culoare, distanţări, etc.), ca şi aşezarea unui "background" atractiv (ca şi alte idei: ataşează un fundal muzical, o animaţie, etc.) – pe lângă faptul că ar putea fi realizate şi fără girul pachetului respectiv, chiar ţin de „zorzoane” (şi adesea, de ridicol), având doar rolul subtil de a compensa lipsa de conţinut, sau faptul că S este foarte banal.

Dar noi avem în vedere numai programe S care merită să fie redate (cu evidenţiere sintactică specifică limbajului subiacent); deci nu ne interesează zorzoanele facilitate prin pachetele menţionate.
Iată deci, provocarea: rescrie lucrurile (pachetul), urmărind scopul principal şi atât (dar evitând să copiezi pur si simplu, din pachetul original – altfel, nu mai vorbim de „rescriere”, ci doar de „copiere” sau plagiere).

Fixarea unui stil de marcare a programelor

Un articol scris în HTML poate fi transformat imediat în LaTeX (ignorând totuşi, aspectele mai particulare, induse cel mai adesea de stilarea CSS):

pandoc -s -f html -t latex articol.html -o artic.tex

Aşa că mai departe vom ignora cazul când A este formulat în HTML; considerăm că A este un articol scris folosind LaTeX într-un fişier "artic.tex", conţinând între altele, mai multe secvenţe de program în diverse limbaje.

Pygments prevede mai multe stiluri, sau şabloane de formatare (le putem lista prin pygmentize -L style); dar firesc este să folosim un acelaşi stil pentru toate secvenţele S. Alegem de exemplu stilul default şi transcriem elementele acestuia într-un fişier separat (urmând să-l includem în A):

pygmentize -S default -f latex > Default.sty

Ulterior, dacă va fi cazul, am avea de schimbat doar fişierul "Default.sty", pentru a reda secvenţele S cu vreun alt stil – de exemplu, colorful, sau dacă redacţia revistei cere „fără culori”, putem alege friendly_grayscale.

"Default.sty" defineşte nişte comenzi (cu prefixul \PY) care asigură marcarea prin anumiţi parametri de culoare sau/şi formă ("italic", "bold", etc.) a porţiunii de text primite ca parametru (un cuvânt-cheie, comentariu, şir de caractere, un operator, etc.). Putem folosi şi direct (sau „cum ne taie capul”), comenzile \PY{}{} (primul argument este 'k', sau 'kt' etc. pentru "keyword", sau 'c', 'c1' etc. pentru "comment", sau 's', 's1' etc. pentru "string", etc. – iar al doilea este secvenţa de caractere interpretată drept "keyword", respectiv comentariu, "string", etc.):

\documentclass[a4paper, 12pt]{article}
\usepackage{Default, xcolor}
\newcommand\B[1]{\small\textbf{#1}}
\newcommand\bksl{\texttt{\textbackslash{}}}

\begin{document}
\noindent\PY{k}{for} -- \B{k}eyword \\
\PY{ow}{perl} -- \B{ow} \\
\small
\PY{c}{\PYZsh{}! \bksl bin\bksl bash} -- \B{c}omment \\
\PY{cp}{\PYZsh{}! \bksl bin\bksl bash} -- \B{cp} \\
\PY{s1}{"<h1\PY{k}{.+}</table>"} -- \B{s}tring, dar\ldots{} cu \B{k} 
                                                pentru \emph{sub\B{s}tring}
\end{document}

Compilând cu xelatex (şi folosind apoi gs pentru a redimensiona pagina, modificând elementul [/CropBox[...]), rezultă fişierul PDF redat în stânga:

  

Schimbând "Default.sty" conform stilului colorful de exemplu (în loc de default) şi recompilând – rezultă fişierul PDF redat în dreapta; se observă uşor, diferenţele între cele două stiluri – de exemplu, pentru a marca "string"-uri în colorful se foloseşte \colorbox{}, nu \textcolor{}.

Pachet LaTeX minimal pentru stilarea programelor

Înfiinţăm în directorul curent un pachet pentru LaTeX, adică în primul rând un fişier "pygjust.sty" (dar cu anumite declaraţii suplimentare obligatorii, faţă de "Default.sty"); denumirea aleasă vrea să sugereze scopul de a angaja pygmentize() dar numai pentru a marca sintactic ("just", fără „zorzoane”) secvenţele de cod sursă existente în fişierul ".tex" care include pygjust:

\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{pygjust}[2022/03/25 pygmentize() the programs within a LaTeX file]
% The same style ("default" for example) applies to all programs:
%     pygmentize -S default -f latex > Default.sty 
% Programs are contained in 'Pyg' environments (based on Verbatim)
% with a single argument: the language of the program.
\RequirePackage{Default, fancyvrb, xcolor}

Prin comanda fără parametri \pygrun, se va invoca intern pygmentize:

\providecommand{\lang}{}
\newcommand\pygrun{pygmentize -P encoding=utf-8 -l \lang\space
     -O escapeinside='÷÷'
     -f latex
     -P texcomments
     -P mathescape
     -o \jobname.vrb \jobname.lst}

Limbajul care trebuie interpretat va fi transmis indirect (nu ca parametru al comenzii \pygrun), prin comanda \lang{} – aceasta va fi redefinită de fiecare dată, pentru a prelua argumentul furnizat environment-ului care conţine codul-sursă curent S.

pygmentize este invocat cu opţiunile esenţiale; "escapeinside" prevede două caractere care pot fi utilizate ca delimitatori, pentru a aplica o comandă LaTeX unei porţiuni de text dinafara comentariilor (şi necuprins într-un "string"); bineînţeles că delimitatorii respectivi trebuie să difere de caracterele folosite în textul programului – de obicei se prevăd '||'; am ales '÷÷', ceea ce este mai greu de tastat – dar am încercat astfel să nu exclud operatorul pipe '|', posibil într-un program Bash.
Analog, "texcomments" şi "mathescape" permit în cadrul liniilor-comentariu, comenzi LaTeX şi respectiv, comenzi de redare a expresiilor matematice.

Textul programului S va trebui înscris într-un environment Pyg:

\newenvironment{Pyg}[1]{%
    \renewcommand{\lang}{#1}  # preia limbajul indicat ca argument
    \VerbatimEnvironment
     \begin{VerbatimOut}{\jobname.lst}}  % executat de \begin{Pyg}{language}
    {\end{VerbatimOut}
     \immediate\write18{\pygrun}
     \input{\jobname.vrb}}  % executat de \end{Pyg}
     % \VerbatimInput{\jobname.vrb}} %% to see as /PY{}{} commands
\endinput  % încheie definiţia pachetului 'pygjust'

\begin{Pyg}{language} defineşte \lang şi înscrie într-un fişier ".lst" textul lui S (folosind – ca şi pachetul verbments de exemplu – \VerbatimEnvironment şi VerbatimOut din fancyvrb). Iar \end{Pyg} va invoca \pygrun (transmiţându-i implicit valoarea dată de \lang, pentru opţiunea -l) şi va scrie rezultatul într-un fişier ".vrb", care apoi prin \input, va înlocui conţinutul iniţial S.

De observat că se folosesc aceleaşi două fişiere ".lst" şi ".vrb", pentru toate secvenţele S întâlnite în A (încât, în fişierul ".vrb" rămâne în final, textul comenzilor PY{}{} pentru ultimul program din A).
Ar fi fost de avut în vedere şi ideea de a obţine fişiere ".vrb" distincte – dacă vrem să modificăm totuşi, rezultatul obţinut din pygmentize; mai ocolit, această idee se poate totuşi realiza înlocuind linia finală input{\jobname.vrb}} prin:

\VerbatimInput{\jobname.vrb}}

Astfel, zona Pyg va fi înlocuită cu un Verbatim care conţine secvenţa de comenzi PY{}{} asociată de pygmentize textului iniţial S; n-avem decât să modificăm una sau alta dintre comenzile respective (eventual, să adăugăm altele) şi să recompilăm A.

Exemplu

Următorul fişier LaTeX conţine un program Bash şi o linie Perl; în ambele cazuri, analizoarele lexicale angajate de pygmentize nu recunosc drept „cuvânt-cheie” termeni ca "perl", sau "grep" – încât încercăm să folosim opţiunea escapeinside (cu "÷÷" ca delimitatori) pentru a aplica \textbf{}, evidenţiind cumva şi asemenea termeni:

\documentclass[a4paper, 12pt]{report}
\usepackage{pygjust}
\begin{document}
Un mic program Bash:
\begin{Pyg}{bash} 
    #! /bin/bash
    cd orar.ienachita.com  #\footnotesize fişierele-orar "*\_NNN\textbf{.html}"
    for f in $(ls) 
    do
        ÷\textbf{perl}÷ -p -e 's/\n/ /' $f | 
        grep -o -P '<h1.+</table>' 
    done
\end{Pyg}

şi o comandă care invocă Perl:
\begin{Pyg}{perl}
    ÷\textbf{perl}÷ -p -i -e 's/<br>(\d+) (\w)/ $1$2/g' cniv2.html
\end{Pyg}
\end{document}

Compilând (cu xelatex -shell-escape, fiindcă în pygjust folosim \write18{}), rezultă fişierul PDF redat în această imagine:

Se vede că escapeinside nu a funcţionat, decât în cazul liniei Perl; în cazul programului Bash, secvenţa prin care am vrut să boldăm "perl" este redată ca atare (iar "÷" apare marcat ca "eroare"). Lexer-ul pentru Bash este (cum şi trebuie) foarte exigent şi se pare că pentru programe Bash, nu se poate folosi escapeinside (căutând pe Internet, constatăm – doar – că şi alţii au păţit-o…); explicaţia comună ar fi aceasta: în Bash, '\' urmat de un caracter este tratat ca "String.Escape" deci de exemplu, "\textbf{}" nu va fi, cum se doreşte, o comandă LaTeX.

Dacă vrem neapărat – putem proceda astfel: activăm în "pygjust.sty" linia VerbatimInput{}} (comentând-o pe cea de deasupra); recompilând fişierul TeX redat mai sus "artic.tex", obţinem în PDF-ul rezultat comenzile PY{}{} – numai că din PDF este mai greu să extragem textul acestor comenzi; prin urmare, să lăsăm în "artic.tex" numai programul Bash, din care eliminăm secvenţa marcată ca escapeinside – recompilând, vom găsi comenzile respective în fişierul "artic.vrb" (redat aici după câteva operaţii de trunchiere a liniilor):

\begin{Verbatim}[commandchars=\\\{\},codes={\catcode`\$=3\catcode`\^=7\catcode`\_=8\relax}]
    \PY{c+c1}{\PYZsh{}! /bin/bash}
    \PY{n+nb}{cd} orar.ienachita.com  
              \PY{c+c1}{\PYZsh{}\footnotesize fişierele-orar "*\_NNN\textbf{.html}"}
    \PY{k}{for} f \PY{k}{in} \PY{k}{\PYZdl{}(}ls\PY{k}{)}
    \PY{k}{do}
        perl \PYZhy{}p \PYZhy{}e \PY{l+s+s1}{
                            \PYZsq{}s/\PYZbs{}n/ /\PYZsq{}} \PY{n+nv}{\PYZdl{}f} \PY{p}{|}
        grep \PYZhy{}o \PYZhy{}P \PY{l+s+s1}{
                            \PYZsq{}\PYZlt{}h1.+\PYZlt{}/table\PYZgt{}\PYZsq{}}
    \PY{k}{done}
\end{Verbatim}

Copiem conţinutul acestui fişier (dar fără a-l despărţi pe linii, altfel în PDF vor rezulta spaţii suplimentare) peste secţiunea Pyg în care era scris programul Bash şi înlocuim "perl" de exemplu cu "PY{ow}{perl}" (la fel, pentru "grep"); ba chiar, pentru a evidenţia şi operatorul pipe înlocuim \PY{p}{|} cu \PY{nn}{|}, iar pentru a evidenţia şi subşirul ".+" din şirul indicat lui grep, înscriem \PY{k+kt}{.+}.
Recompilând, obţinem acum, în PDF:

N-avem decât să adăugăm în "artic.tex" celelalte programe existente iniţial şi să reactivăm linia "\input{\jobname.vrb}}" din pygjust.sty (comentând pe cea aflată dedesubt – pe care o activasem temporar, numai pentru programul Bash).

Să mai observăm că în "artic.vrb", comenzile \PY{}{} respective sunt înscrise într-un Verbatim, pe care s-a activat opţiunea commandchars=\\\{\} – care permite recunoaşterea comenzilor LaTeX (care au forma "\command"); de fapt, putem şi elimina, la includerea în artic.tex, liniile "\begin{Verbatim}" şi "\end{Verbatim}" – înscriind numai textul de comenzi interior.
În unele situaţii este recomandabil să folosim direct Verbatim – având grijă să prevedem commandchars – în loc de pygmentize; de exemplu, adesea redăm o secvenţă de program R împreună cu tabelul de date produs în consolă de execuţia acesteia şi bineînţeles că putem folosi şi pygjust pentru aceasta; dar este mai bine dacă separăm lucrurile: redăm numai programul prin pygjust, iar dedesubt redăm tabelul de rezultate folosind Verbatim (şi fiindcă am folosit commandchars, putem de exemplu să marcăm antetul tabelului prin \emph{}, iar unele dintre datele obţinute prin \textbf{}; în plus, putem folosi şi alte opţiuni pentru Verbatim – de exemplu fontsize=\footnotesize, pentru a produce tabelul respectiv cu o mărime de font mai mică).

vezi Cărţile mele (de programare)

docerpro | Prev | Next