momente şi schiţe de informatică şi matematică
anti point—and—click

Adăugarea cuprinsului, folosind XML::Twig

TOC | XML | XML::Twig | perl
2007 aug

Un articol are o temă de bază, în jurul căreia reconstituie anumite cunoştinţe, pune sau reia anumite probleme şi conturează anumite soluţii. TOC ("Table Of Content") este un element suplimentar în structura articolului, putând fi generat automat în două moduri:
dinamic - după ce articolul este încărcat în pagina Web de pe care s-a făcut cererea, se activează o anumită funcţie javascript care colectează titlurile secţiunilor existente la acel moment, creează TOC şi-l inserează în pagină (nu în fişierul sursă!);
static - se foloseşte un program de transformare: primind ca parametru numele fişierului care conţine sursa articolului, citeşte conţinutul, îl transformă în memorie prin inserarea elementul TOC şi scrie apoi acest nou conţinut pe fişier.

În primul caz, sursa nu conţine TOC, ci doar un <script> final care indică funcţia de executat după ce browserul va fi terminat de încărcat fişierul sursă; în varianta bazată pe transformare, TOC ajunge să fie conţinut în fişierul sursă şi browserul este scutit de operaţii suplimentare (pentru generarea TOC-ului, ca în primul caz).

În mod ideal, articolul se scrie "într-o juma' de oră" şi nu necesită nici o revizuire (dar probabil, nici TOC). În general, un articol se scrie în măcar câteva zile, implicând pe parcurs diverse revizuiri şi adăugiri de secţiuni; pe parcurs este preferabil ca TOC-ul să fie generat dinamic (generarea statică la un anumit moment, ar implica repetarea generării statice după fiecare revizuire / adăugire ulterioară; mai mult probabil, va trebui ca în prealabil să se şteargă din fişier vechiul TOC şi diverse alte elemente create prin precedenta rulare a programului de transformare). În schimb, dacă s-a ajuns la momentul stabilizării structurii articolului, atunci devine preferabilă generarea definitivă ("static") a TOC-ului.

Structura tipică de articol

Un articol (publicat într-o revistă electronică, pe un blog, chiar şi într-o revistă tipărită) prezintă în general, următoarele elemente de structură:

TITLE  apare de obicei într-un element <H1>
SUBTITLE?
KEYWORDS? (cuvinte-cheie, pentru a facilita operaţiile de căutare)
SUMMARY sau "abstract" (sintetizează conţinutul/tema articolului)
AUTHOR (autorii articolului)
DATE  (data publicării)
CONTENT care subordonează următoarele elemente, unele fiind opţionale:
    TOC? (table of content)
    SECTION+  una sau mai multe secţiuni; O secţiune include:
        SECTION_TITLE (de obicei în elemente <H2>...<H6>; 
                       titlul secţiunii va fi inclus în TOC)
        SECTION_TEXT+  (conţinut de tip text; 
                        foloseşte în principal elemente <p>)
        SECTION_CODE* (conţinut special, de exemplu cod-sursă de program; 
                       foloseşte de obicei <pre>)
        SECTION_LINKS* (specifică resurse externe legate de conţinut)
        SECTION_IMAGES* (specifică fişiere-IMG asociate secţiunii)
        SUB_SECTION* (cu acelaşi gen de conţinut că un element SECTION)
BIBLIOGRAPHY?  articole/lucrări referite sau recomandate în articol     

Semnele ?, *, + folosite mai sus în descrierea structurii, specifică—la fel ca în DTD-ul unui document XML—de câte ori poate să apară elementul: de zero ori sau o singură dată, respectiv de zero sau mai multe ori, respectiv o dată sau de mai multe ori. Indentarea este folosită mai sus pentru a arăta "ramurile" (sau elementele subordonate) unui element; astfel, CONTENT are o ramură opţională TOC şi una sau mai multe ramuri SECTION.

Observaţie. Standardizarea structurii articolelor este întreprinsă deja de mulţi ani, în diverse direcţii; sistemul TEX include pachete de macrodefiniţii pentru articole cu structură similară celeia descrise mai sus (TEX este opera lui Donald Knuth, autorul celebrei cărţi Art of Computer Programming, tradusă şi la noi în 1974-76 cu titlul… "Tratat de programare a calculatoarelor"); HTML 5 deja prevede elemente specifice structurii unui articol (vezi New elements in HTML 5), iar DocBook este un limbaj de tip XML care defineşte structuri standard pentru documentaţii tehnice, cărţi sau articole ştiinţifice.

Principiile generării automate a TOC-lui

Elementele HTML <h1>, <h2>, <h3>, ... ("heading elements") sunt folosite pentru a marca secţiunile (şi conţin titlurile secţiunilor respective). Pe aceasta se şi bazează generarea automată a TOC-ului: mai întâi, se colectează elementele "heading" din cadrul documentului; fiecăruia dintre elementele colectate, i se adaugă un atribut ID cu valoare unică (secţiunea va fi referită ulterior prin acest ID); apoi, se creează elemente <a> având ca valoare a atributului href tocmai id-ul fixat anterior (prefixat cu "#") şi se inserează link-urile rezultate, drept conţinut al elementului TOC (identificat undeva în articol printr-un id corespunzător). Pentru o exemplificare, să presupunem că articolul arată astfel:

<div id="TOC"> </div>   <!-- locul TOC-ului care va fi produs automat -->

<h2>Titlul primei secţiuni</h2>
<p>Primul paragraf al primei secţiuni</p>
<p>Al doilea paragraf al primei secţiuni</p>

<h3>Titlu de sub-secţiune a primei secţiuni</h3>
<p>Primul paragraf al sub-secţiunii</p>
<p>Al doilea paragraf al sub-secţiunii</p>

După operaţiile descrise mai sus (efectuate prin javascript, sau de programul de transformare), vom obţine:

<div id="TOC">                   <!-- TOC-ul rezultat -->
<a href="#gen_1">Titlul primei secţiuni</a>
<a href="#gen_2">Titlu de sub-secţiune a primei secţiuni</a>
</div>

<h2 id="gen_1">Titlul primei secţiuni</h2>
<p>Primul paragraf al primei secţiuni</p>
<p>Al doilea paragraf al primei secţiuni</p>

<h3 id="gen_2">Titlu de sub-secţiune a primei secţiuni</h3>
<p>Primul paragraf al sub-secţiunii</p>
<p>Al doilea paragraf al sub-secţiunii</p>

Bineînţeles, ideea este aceea de a asigura această interacţiune: când utilizatorul va face click pe unul dintre link-urile prezente în TOC, browserul va aduce în fereastra curentă secţiunea din articol referită de acel link.

Desigur, pentru elementul TOC (şi pentru link-urile conţinute de acesta) trebuie prevăzută şi o anumită modalitate de prezentare. În acest scop, putem folosi—după [1]—următoarea idee: ambalăm fiecare link conţinut în TOC, într-un element <div class="level_N"> (unde "N" este 1, 2, 3, ... corespunzător "heading"-ului din care provine acel link); prevedem stilări corespunzătoare fiecărei valori "level_N" (şi desigur, prevedem şi o stilare corespunzătoare containerului identificat prin TOC). Astfel, TOC-ul exemplificat mai sus devine:

<div id="TOC"> 
<div class="level_2"><a href="#gen_1">Titlul primei secţiuni</a><div>
<div class="level_3"><a href="#gen_2">Titlu de sub-secţiune a primei secţiuni</div>
</div>

iar ca exemplu de stilare, putem adăuga într-un fişier CSS pentru toate articolele de pe site:

#TOC { /* stiluri pentru containerul care are ID="TOC" */
    border-left: 2px solid #cc3333;
    margin: 0px 0px 20px 20px; padding: 5px;
    line-height: 1.5em;
}
/* pentru elementele conţinute de TOC, care au class="level_N"
   se asigură o indentare corespunzătoare faţă de marginea stângă 
   a containerului TOC */
#TOC .level_2 { margin-left: 1em; }  /* pentru secţiune de "heading" H2 */
#TOC .level_3 { margin-left: 3em; }  /* pentru secţiune de "heading" H3 */
#TOC .level_4 { margin-left: 5em; }  /* pentru secţiune de "heading" H4 */

Funcţiile javascript necesare transformărilor indicate mai sus sunt redate în [1]. Se poate observa pe articolul citat, că se mai face o transformare pe lângă cele menţionate mai sus: se inserează fiecărui "heading" de secţiune un anumit element, reprezentat vizual prin "^TOP^"; la click pe acest element, fereastra "scrollează" la poziţia originală. Şi într-adevăr, investigând cu instrumentele obişnuite în Firefox ("Display Id & Class Detail", "View Javascript", etc.) constatăm că "heading"-urile secţiunilor sunt completate (desigur tot automat, prin intermediul funcţiilor javascript respective) astfel:

<h2 id="gen_1">Titlul primei secţiuni
   <div class="scrolltop pointer" title="Go to the top of the page" 
        onclick="function(){scrollTo(0,0);}"> ^TOP^ </div>
</h2>

Obs. Este adevărat că în articolul citat, nu se menţionează explicit şi această ultimă transformare. Dar un articol publicat pe Internet nu este la fel ca un articol tipărit, iar unul de literatură nu este la fel ca unul referitor la Web! A citi cu folos un articol de Web, înseamnă şi a investiga corespunzător la nivelul sursei HTML, a codului javascript, a codului CSS, etc.; de obicei, "amănuntele" (dar uneori - partea cea mai interesantă!) sunt puse în mod tacit, la dispoziţia celui care are spirit de observaţie şi interes de analiză.

Crearea şi inserarea TOC-ului printr-un program Perl

Avem de modificat fişierul sursă al articolului, completându-l cu TOC. Programul de transformare va angaja modulul XML::Twig. Întâi însă, prezentăm câteva elemente ale limbajului perl, suficiente pentru abordarea programului intenţionat aici (pentru o iniţiere mai completă, vezi de exemplu perlintro.html).

Elemente de limbaj Perl şi de sistem Linux

Limbajul perl oferă nenumărate posibilităţi pentru investigarea şi modificarea de fişiere text. CPANComprehensive Perl Archive Network pune la dispoziţie majoritatea modulelor perl (peste 12000, din 1993 încoace), acoperind toate categoriile de prelucrări previzibile (de la manipularea textului de orice tip, până la protocoale de reţea, integrarea bazelor de date şi grafică).

Ne amintim de exemplu, că pentru a folosi funcţia strlen() în cadrul unui program C, trebuie să asigurăm "legarea" programului cu biblioteca string.h—printr-o declaraţie # include <string.h>; analog în perl: pentru a folosi funcţionalitatea încorporată într-un modul—fie acesta, String::Util, trebuie să "includem" acest modul în program, prin use String::Util:

#!/usr/bin/perl  
# programul Perl "test-string.pl"

use strict;  # restricţionează construcţiile "nesigure"
use String::Util ':all'; # vezi documentaţia modulului, la CPAN

my %optiuni = ( numerals => 0,  # şirul generat conţine eventual şi cifre
                strip_vowels => 0 );  # se permit vocale în şir

my $password = randword(10, %optiuni); # invocă funcţia String::Util::randword 
                      # un şir aleatoriu de 10 caractere, conform cu 'opţiuni'
print "\nS-a generat aleatoriu parola: $password\n";
my @password = split "", $password;  # Împarte şirul în caractere individuale, 
                                     # returnând tabloul rezultat.
print "Tabloul de caractere corespunzător parolei este: @password\n\n";
print "Parola este criptată prin: " . randcrypt("$password") . "\n";

perl prevede trei categorii de variabile: scalari, liste (tablouri, sau "array") şi hash; distincţia se face prin prefixarea numelui variabilei cu unul dintre caracterele $ (în cazul variabilelor scalare), @ (pentru array) şi % (pentru hash).

Mai sus, apare variabila scalară $password, tabloul @password (care este o variabilă diferită de scalarul cu acelaşi nume) şi hash-ul %optiuni - variabile locale (fiind declarate cu my). Un scalar poate să aibă valori numerice, alfanumerice sau referinţe ("pointeri" cum am zice în C, la alte variabile, sau la funcţii). Un hash este un set de perechi de forma CHEIE => VALOARE; astfel, hash-ul %optiuni are două chei denumite conform specificaţiilor modulului, "numerals" şi "strip_vowels", având valori numerice (=> 0).

Putem vedea un hash ca pe un array cu număr par de elemente, în care pe rangurile impare stau "cheile", iar pe rangurile pare stau "valorile" asociate - numai că într-un hash perechile respective nu sunt şi ordonate (nu pot fi accesate prin indexare, ca în cazul tablourilor).

Programul redat mai sus a fost scris folosind un editor de text obişnuit şi a fost salvat cu numele "test-string.pl" (în directorul /home al user-ului vb, redat în linia de comandă a unui terminal obişnuit printr-un prompt precum vb@debian:~ unde 'debian' este numele de localhost folosit pe sistemul respectiv); pe disk, "test-string.pl" nu diferă cu nimic faţă de un fişier obişnuit, însă el corespunde totuşi unui program "executabil" şi pentru ca sistemul să facă diferenţa trebuie ca fişierului respectiv să i se confere explicit statutul de "executabil":

vb@debian:~$ chmod 0755 test-string.pl
vb@debian:~$
vb@debian:~$ ls -l test-string.pl
-rwxr-xr-x 1 vb vb 519 2007-08-26 12:19 test-string.pl

chmod permite schimbarea permisiunilor de acces asupra fişierului; şirul iniţial rwx din rezultatul comenzii ls (care listează informaţii privind fişierul) indică drepturile proprietarului (cel care a creat fişierul): read (r), write (w) şi eXecute (x), iar următoarele două grupuri de câte 3 caractere ('r-x') indică drepturile asupra fişierului pentru user-ii care fac parte din acelaşi group cu proprietarul şi respectiv, pentru ceilalţi useri (au drept de citire şi drept de execuţie, nu şi de scriere).

Acum programul poate fi executat (sau măcar, lansat în execuţie...):

vb@debian:~$ ./test-string.pl
Can't locate String/Util.pm in @INC 
                 (@INC contains: /etc/perl /usr/local/lib/perl/5.8.8 ...
BEGIN failed--compilation aborted at ./test-string.pl line 8.
vb@debian:~$

Mesajul de eroare obţinut spune că modulul String::Util nu apare în lista @INC (în care sunt înregistrate căile ("path") de acces către diversele module instalate pe sistem); cu alte cuvinte, acest modul trebuie instalat manual, înainte de a putea executa "test-string.pl":

vb@debian:~$ sudo perl -MCPAN -e 'install String::Util'

S-a accesat CPAN, s-a descărcat de acolo şi apoi s-a instalat pe sistem modulul indicat; desigur, "instalarea" nu o poate face decât "superuserul" root (nu "vb@debian:~$" ci "debian:/home/vb#")—de aceea s-a folosit ca "prefix" comanda sudo prin care un user obişnuit se poate substitui unui alt user (chiar şi lui root, dacă pe sistem există configurări adecvate). După instalarea modulului, programul poate fi executat:

vb@debian:~$ ./test-string.pl
S-a generat aleatoriu parola: NKyRIYOjB6
Tabloul de caractere corespunzător parolei este: N K y R I Y O j B 6
Parola este criptată prin: 3NbtaZGMryRmU

Programul poate fi rulat din nou, rezultând desigur o altă "parolă"; dacă în fişierul sursă se schimbă valoarea cheii "numbers" (din 0, în 1) şi (după salvare) se execută din nou, atunci se va obţine o "parolă" constituită numai din cifre; dacă se elimină opţiunea respectivă (prin comentare, adică prefixând cu "#": # numerals => 0,) atunci "parola" va conţine numai litere (nu şi cifre); analog se poate experimenta şi cu opţiunea "strip_vowels".

Generarea TOC şi transformarea fişierului sursă, folosind XML::Twig

Ideea comună pentru modulele din spaţiul de nume (namespace) XML:: sau HTML:: constă în reprezentarea documentului în memorie printr-un arbore; elementul rădăcină al documentului (în cazul HTML, de obicei elementul <html>) devine nodul root al arborelui, iar celelalte elemente din document devin subarbori ai nodului root în memorie. Adesea, cuvintele-cheie folosite (ca denumiri de funcţii, de exemplu) derivă din tree (pentru "arbore"), twig (pentru "ramură", sau "subarbore"), parse ("analiza" documentului), handler sau callback, etc.

Aceste module sunt de tip OO ("Object Oriented"); întâi trebuie să se instanţieze un obiect, indicând eventual şi valori potrivite pentru posibilii parametri de construcţie preconizaţi în definiţia modulului; prin intermediul obiectului construit astfel, vor putea fi accesate apoi metodele implementate în modul (cam în toate limbajele e la fel).

De exemplu (şi este util de observat analogia posibilă cu limbajul C), prin my $tw = new XML::Twig(); se construieşte un obiect de memorie conţinând datele şi "pointeri" la metodele specifice modulului XML::Twig iar variabila $tw reţine o referinţă la obiectul construit; $tw->parsefile($f) va accesa şi executa funcţia XML::Twig::parsefile - prin care se va deschide fişierul cu numele indicat de parametrul $f (presupus a fi un document XML) şi se va analiza conţinutul fişierului pentru a construi în memorie arborele corespunzător iar în final, se va înscrie în variabila internă $tw->root o referinţă la rădăcina arborelui construit; mai departe, de exemplu $tw->root->first_child va adresa "primul" subarbore, etc.

#!/usr/bin/perl -w
use strict;
use XML::Twig; 

my $p_id = "igr1"; # Valorile atributului ID pentru "headings": 
                   # "igr1" la primul, apoi "igr2", etc.
my $tw = new XML::Twig ();
$tw->setTwigHandler(qr/h\d/, \&in_toc); 
# asociază fiecărui "heading" (<h1>, <h2> etc.) o referinţă la funcţia "in_toc"
# ("in_toc" va opera anumite modificări asupra nodurilor/ramurilor respective)

$tw->parsefile( "grila.html");
#    deschide fişierul şi construieşte arborele corespunzător în memorie; 
#    apoi, transformă arborele iniţial printr-un mecanism de genul următor:
#    parcurge arborele, executând "handler"-ele asociate nodurilor vizitate

$tw->print_to_file("grila-1.html");
#    creează fişierul şi scrie arborele rezultat (deci documentul transformat)

sub in_toc {   # 'sub' introduce o funcţie ("subrutină")
   my( $twig, $hi)= @_;  # Salvează în variabila internă $twig o referinţă la $tw
                         # şi în $hi o referinţă la nodul "heading" din care 
                         # este apelată funcţia. 
   $hi->set_id($p_id);  # Creează în nodul referit de $hi atributul ID = 
                        # = valoarea curentă din $p_id
   my $toc = $twig->elt_id('toc'); # Obţine în $toc o referinţă la nodul 
                                   # care are ID == "toc"
   my $h = $hi->copy; $h->del_att('id');
           #  Obţine în $h o clonă a subarborelui cu rădăcina referită de $hi
           #  şi şterge atributul ID în copia respectivă
   my $t = $h->tag; $t =~ s/\D+//g;  # Extrage în $t cifra din "heading" 
                                     # (din <h1>, <h2> etc.); această cifră va fi
                                     # necesară pentru "N"-ul din "level_N".  
   $h->set_tag('a'); $h->set_att('href' => "#".$p_id);
           # Clona referită de $h are acum "tag"-ul 
           # <a href=valoarea curentă din $p_id>.
   my $d = XML::Twig::Elt->new('div' => {'class' => "level".$t}, $h);
           # Se creează un nod <div class="level $t"> care devine "parent" pentru 
           # clona referită de $h; practic, $d referă acum o structură ca
           # <div class="level2"><a href="igr3">...</a></div>.
   $d->paste(last_child => $toc);
           # Structura indicată de $d este însăilată ca ultim-fiu 
           # al subarborelui de rădăcină referită de $toc.
   $d = XML::Twig::Elt->new('div' => {
                                       'class' => "scrollTop pointer",
                                       'title' => "Go to the top of the page",
                                       'onclick' => "new function() {scrollTo(0,0);}" 
                                     }, "^TOP^");
   $d->paste(first_child => $hi);
   # Creează o structură <div> cu atributele "class', 'title' şi 'onclick' arătate
   # în constructor, reţine în variabila existentă $d o referinţă la această structură
   # şi apoi, "paste"-ază $d ca prim-fiu în subarborele cu rădăcina indicată de $hi.
      
   $p_id++; # Incrementează valoarea ID, pregătind execuţia funcţiei pe următorul 
            # "heading" din document
}

La încercarea de a rula programul a apărut o problemă legată de caracterele folosite în "grila.html" (unele nu sunt recunoscute automat de către XML::Twig); e vorba de caractere introduse (la editarea fişierului respectiv) prin HTML Character Entities References (nume simbolice atribuite caracterelor, formate cu o sintaxă specială; în loc să se înregistreze pe disk codul numeric al caracterului, se înregistrează şirul de caractere care constituie numele caracterului). De exemplu am folosit caracterul "nestandard" (nu există pe tastatură) "—", şi pentru a-l introduce am folosit numele lui de entitate HTML: &mdash;; ori XML::Twig recunoaşte doar cele 5 entităţi de caracter specifice XML (numele "&amp;" pentru "&", "&lt;" pentru "<", etc.); modulul pune la dispoziţie şi metode de specificare a altor entităţi de caracter care trebuie recunoscute - dar am preferat (fiind vorba de puţine asemenea situaţii, în cazul "grila.html") să folosesc direct următorul program de tip "one-liner":

perl -pi.bak -e "s/\&mdash;/\&\#8212;/g; s/\&hellip;/\&\#8230;/g" \
        grila.html dropword.html

prin care apelând din linia de comandă interpretorul perl, se caută în fişierele indicate ("grila/html" şi "dropword.html") secvenţele de caractere "&mdash;" şi "&hellip;" (fiecare măsoară 7 caractere) şi se înlocuiesc respectiv cu secvenţele de câte doi octeţi reprezentate prin &#8212; şi &#8230; (care apar ca fiind tot de câte 7 caractere, dar secvenţele respective sunt Numeric character references adică specifică indexul numeric al caracterului în cadrul setului de caractere al documentului şi ca urmare, pe disk se reprezintă ca numere deci pe doi octeţi şi nu pe 7).

Subtilităţi: care scrollTo()?

După modificarea astfel în "grila.html", programul de transformare a furnizat "grila-1.html"; încărcând "grila-1.html" în browser (în Firefox) am putut verifica că s-a integrat TOC-ul în articol iar legăturile de la link-urile existente în TOC la secţiunile referite şi invers, într-adevăr funcţionează. Însă, când în sfârşit am înlocuit "grila.html" existent pe site cu "grila-1.html", am constatat că legăturile "inverse", de la elementele "^TOP^" la începutul paginii în care este afişat articolul - nu funcţionează! (la click pe un "^TOP^", fereastra scrolează eventual până ce acel "^TOP^" ajunge la marginea superioară a ecranului şi nu până la începutul paginii). Am dedus că e o problemă legată de funcţia scrollTo() care este activată la click-ul respectiv. Până la urmă, am stabilit (cel puţin, pentru Firefox) că această funcţie trebuie apelată explicit ca metodă a obiectului global window, adică în programul de mai sus este necesară această corectură:

# ..........
   $d = XML::Twig::Elt->new('div' => {
           'class' => "scrollTop pointer",
           'title' => "Go to the top of the page",
           'onclick' => "new function() { <b>window.</b>scrollTo(0,0); }"
        }, "^TOP^");
# ..........

Desigur, nu-i aceasta singura modalitate de rezolvare şi poate nici cea mai bună (apropo de cross-browser), cu atât mai mult cu cât în funcţiile javascript la care ne-am referit anterior se foloseşte simplu "scrollTo(0,0)" (fară prefix window.); însă acolo, "articolul" este tratat ca o pagină Web independentă, nu în contextul altor obiecte existente ca în cazul de faţă. În varianta cu javascript, "scrollTo" este apelată dintr-o funcţie care nu face parte din fişierul "grila.html" (şi ca urmare, "scrollTo" apare implicit ca membru al obiectului global "window"), pe când acum este apelată chiar din document (şi probabil trebuie acum specificat că e vorba de "scrollTo" al obiectului global "window" şi nu de "scrollTo" moştenit în "document" - conform principiului că metoda "moştenită" poate fi eventual suprascrisă în obiectul derivat).

Bibliografie

[1] https://evolt.org/Automatic_TOC_Generation by Mihai Bazon (2002)

vezi Cărţile mele (de programare)

docerpro | Prev | Next