De când mă știu, exersersarea de genul temă pentru acasă nu s-a lipit de mine niciodată. Am destestat cumplit exercițiile izolate undeva-n vid, însă mi-a plăcut teribil de mult să fac lucruri ce oricum m-au învățat mult mai mult.

Referințe
Este și motivul de căpătâi pentru care modalitatea de antrenament sugerată de Robert C. Martin în Clean Coder, acele kata, nu prea funcționează, ritualul fiind așa: deschid una, mă uit cu ochi umezi de vițel la poartă nouă, o închid și mă apuc de altceva. Am încercat, aia e.
În schimb, ce mai fac din când în când pentru întreținere (și ca spectacol de varieté) este să încropesc un mic proiect cu valențe practice sau să umblu la altul existent, de multe ori unul foarte vechi, de preferat în alt limbaj decât cel folosit curent, cu scopul de:
– a configura întreg mediul necesar pentru a rula din nou;
– actualiza dependințele, eventual înlocui de tot o parte din ele, în caz că se impune;
– a face curățenie prin codul sursă ori, de e nu, a folosi paradigme noi.
Așadar, urmează descrierea (cu mențiuni despre darea de seamă a repunerii pe linia de plutire) unei mici aplicații ce-a oferit unui sistem de bibliotecă digitală funcționalitățile necesare pentru generarea elementelor bibliografice (citări și bibliografii întregi).
Fluxul era gândit cam așa: utilizatorul își marca diverse paragrafe pe document (carte, revistă etc.) și apoi putea genera o colecție de citări sau o bibliografie întreagă formatată în stilul dorit, căci există o multitudine de stiluri (mii și mii).
Pentru gestionarea electronică a acestor elemente există (și poate nu e singurul) un limbaj bazat pe XML, numit Citation Style Language (CSL). Cum am ajuns la el, nu mai țin minte și nici nu mai contează. Bref e că ieșea din discuție să implementăm un procesor CSL, iar singurul proiect activ rezonabil de capabil și activ menținut la acea vreme (circa 2012) era citeproc-js. Proabil reiese acum de ce-a fost o aplicație separată.
Găzduirea aplicației
Etapa următoare a fost găzduirea – fiind un mediu .NET, rulat sub IIS, ne-am pus problema dacă am putea să rulăm o aplicație NodeJs ca un website separat tot în IIS. La vremea respectivă, o opțiune interesantă a fost iisnode. Acum, chit că nu-i abandonat, este lăsat de izbeliște, ultima versiune oficială având suport declarat pentru IIS v8.
Așadar, deși există o modalitate mai nouă care facilitatează găzduirea, m-am întrebat dacă în continuare poate fi o opțiune valabilă, măcar ca simplă curiozitate. Pe scurt, da: pe IIS v10 (Windows 11) și NodeJs v22.13.1, am reușit să rulez aplicația bine mersi, fiind totuși necesare câteva ajustări:
Prima, ține de-o avertizare vis-a-vis de utilizarea clasei Buffer în fișierul intercept.js livrat cu iisnode (C:\Program Files\iisnode):
DeprecationWarning: Buffer() is deprecated
due to security and usability issues.
Please use the Buffer.alloc(), Buffer.allocUnsafe(),
or Buffer.from() methods instead.
O soluție rapidă a fost să operez înlocuirea sugerată, în acel fișier, în jur de linia 169 (este singurul apel, imposibil de ratat):
return Buffer.alloc(data, typeof encoding === 'string'
? encoding
: 'utf8')
În sfârșit, înainte de-a continua cu restul operațiunii, a fost neceară configurarea tuturor părților în mișcare astfel încât să pot depana la buton direct din VS Code. În primul rând, în web.config am modificat modalitatea de invocare a Node JS:
<iisnode
loggingEnabled="true"
nodeProcessCommandLine="C:\...\node.exe --trace-deprecation --inspect"
/>
Relevantă aici este opțiunea –inspect. Și, deși n-are legătură cu scopul meu imediat, având în vedere că pe-aici este multe lucruri noi, am adăugat și opțiunea –trace-deprecation, pentru a putea identifica locurile unde sunt folosite artefacte depreciate.
În al doilea rând, proiectul VS Code (fișierul [NUME].code-workspace) are și el nevoie de puțină iubire (localRoot și remoteRoot sunt calea către aplicație… find locală, ambele sunt identice, restul bănuiesc că se-nțeleg de la sine):
"launch": {
"configurations": [{
"type": "node",
"name": "DEBUG IIS NODE",
"request": "attach",
"address": "pawntajs.local",
"port": 80,
"localRoot": "D:\\...\\src",
"remoteRoot": "D:\\...\\src"
}]
}
Platforma
Prima problemă reală a fost o avertizare emisă din măruntaiele Restify, framework-ul folosit pentru expunerea serviciilor web:
Access to process.binding('http_parser') is deprecated.
Întâmplător a fost mult mai ușor să-l înlocuiesc cu ExpressJs decât să-mi învârt mințile-n jurul problemei – mi-a luat o jumătate de oră să ajustez opțiunile Restify versus 10 minute să instalez ExpressJs și să fac câteva corecturi punctuale.
Controlul accesului
De vreme ce-a fost gândită și utilizată ca o aplicație strict internă – adică inaccesibilă în afara rețelei – controlul accesului este destul de simplu:
– în web.config sunt câteva reguli pentru prevenirea accesării anumitor directoare ale aplicației (data, engine, node_module, iisnode, logs);
– aplicația propriu-zisă are un middleware ce verifică existența unei chei (api key) – mai întâi în header-ul HTTP x-api-key, apoi, de chiulește, în parametrul apiKey din URL – și o compară cu cea configurată în engine/config.js.
Funcționalitățile, pe scurt
Funcționalitatea propriu-zisă este suficient de sumară încât să adauge o tușă de redunanță mențiunii pe scurt, cerințele fiind:
– să ofere o interfață pentru generarea unei singure citări pentru o singură referință, pentru generearea unei bibliografii pentru o colecție de referințe și, în plus, pentru obținerea stilurilor și localizărilor disponibile pentru cele două operațiuni;
– formatarea rezultatului să fie într-un format HTML ușor modificat față de cel standard produs de citeproc-js, deoarece ulterior era centralizat într-un document DOCX obținut prin conversia din HTML, pe baza unui șablon.
În ceea ce privește interfața, punctele de acces sunt:
Metoda HTTP | URL | Note |
---|---|---|
GET | /citations/available-styles | Lista cu stilurile CSL disponibile, perechi cheie-valoare (cheile sunt codurile stilurilor CSL, valorile sunt denumirile stilurilor) |
GET | /citations/available-locales | Lista cu localizările disponibile, doar identificatori |
POST | /citations/create | Generarea unei singure citări pentru o singură referință |
POST | /citations/create-bibliography | Generearea unei bibliografii pentru o colecție de referințe |
Să dăm și câteva exemple. În primul rând, pentru crearea unei citări individuale (stilul Romanian Humanities, în limba engleză):
{ "Style":"romanian-humanities", "Locale":"en-US", "Book": { "DocumentId":1, "DocumentTitle":"Structuri de date si algoritmi", "DocumentType":1, "SerialNumber":"978-973-650-213-2", "SerialNumberType":"ISBN", "PublisherName":"Editura Albastra", "PublishingPlaceName":"Cluj-Napoca", "CollectionTitle":"223 Seria PC", "Authors": [ { "FirstName": "Iosif", "LastName": "Ignat" }, { "FirstName": "Claudia-Lavinia", "LastName": "Ignat" } ] } }
… va produce următorul rezultat:
{ "Success": true, "Citation": "Ignat Iosif, Ignat Claudia-Lavinia, <i>Structuri de date si algoritmi</i>, 223 Seria PC, Editura Albastra, Cluj-Napoca." }
Apoi, pentru generarea unei bibliografii (stilul Chicago Manual of Style 17th edition (full note), în limba română):
{ "Style":"chicago-fullnote-bibliography", "Locale":"ro-RO", "Items": [{ "Book":{ "DocumentId":4480, "DocumentTitle":"Inselaciunea in noul Cod penal", "Authors":[{ "AuthorId":1856, "FirstName":"DUVAC", "LastName":"Constantin" }], "MagazineTitle":"Dreptul 1/2012", "DocumentType":3, "SerialNumber":"1018-043501201", "SerialNumberType":"ISSN", "PublisherName":"Uniunea Juriştilor din România", "PublishingPlaceName":null, "PublicationYear":2012 }, "Page":107 }, { "Book": { "DocumentId":1, "DocumentTitle":"Structuri de date si algoritmi", "DocumentType":1, "SerialNumber":"978-973-650-213-2", "SerialNumberType":"ISBN", "PublisherName":"Editura Albastra", "PublishingPlaceName":"Cluj-Napoca", "CollectionTitle":"223 Seria PC", "Authors": [ { "FirstName": "Iosif", "LastName": "Ignat" }, { "FirstName": "Claudia-Lavinia", "LastName": "Ignat" } ], "PublicationYear": 2006 }, "Page": 110 }] }
… va furniza următoarele date de ieșire:
{ "Success": true, "Bibliography": { "BibStart": "", "BibEnd": "", "EntrySpacing": 0, "LineSpacing": 1, "MaxOffset": 0, "HangingIndent": true, "Title": "Bibliography", "Entries": [ "<html> <head></head> <body> <p class=\"bib-entry\"> DUVAC, Constantin. „Inselaciunea in noul Cod penal”. <i>Dreptul 1/2012</i>, Uniunea Juriştilor din România, 2012. </p> </body> </html>", "<html> <head></head> <body> <p class=\"bib-entry\"> Iosif, Ignat, și Ignat Claudia-Lavinia. <i>Structuri de date si algoritmi</i>. 223 Seria PC. Cluj-Napoca: Editura Albastra, 2006. </p> </body> </html>" ] }, "Citations": { "ITEM-4480-5": { "Id": "ITEM-4480-5", "Citation": "Constantin DUVAC, „Inselaciunea in noul Cod penal”, <i>Dreptul 1/2012</i> (Uniunea Juriştilor din România, 2012), 107.", "NoteIndex": 1 }, "ITEM-1-6": { "Id": "ITEM-1-6", "Citation": "Ignat Iosif și Ignat Claudia-Lavinia, <i>Structuri de date si algoritmi</i>, 223 Seria PC (Cluj-Napoca: Editura Albastra, 2006), 110.", "NoteIndex": 2 } } }
Datele de intrare erau colectate din interacțiunea utilizatorilor cu documente-n format digital (scanate, OCR-izate, adnotate fiecare cu metadatele de rigoare); iar rezultatele pot fi apoi integrate oriunde, nu doar în documente DOCX. Dacă seamănă cu ce face Zotero este pentru că de-acolo ne-am inspirat și nu doar atât, citeproc-js stătea, poate încă stă, la baza sa.
Despre citeproc-js, tot pe scurt
Nu o să intru în descrierea interfeței de utilizare, doar aș reproduce manualul de utilizare, extrem de bun pentru o unealtă atât de nișată. Ca o primă adnotare, este de-apreciat că vine ca un simplu fișier de copiat oriunde dorești și importat apoi în modulul ce-are nevoie de dânsul. În schimb, poate că ar fi mai util să intru în unele, să le zicem, ciudățenii.
Prima ar fi modalitatea de parametrizare a localizării folosite la producerea artefactelor – citări, bibliografii etc. Pe de o parte, în fișierul CSL poate exista un atribut default-locale pe nodul style. Pe de altă parte, constructorul acceptă un parametru opțional lang. Acesta este însă ignorat dacă default-locale există, ceea ce produce un comportament cu potențial imprevizibil dacă nu se transmite și al patrulea parametru opțional, forceLang, cu o valoare evaluabilă la true.

Schema vagă de principiu a dependințelor citeproc-js
Poate că natura dinamică a determinării limbii necesare a produs și discrepanța dintre transmiterea stilului (XML serializat sau un obiect JavaScript) și transmiterea localizării (ca identificator simplu), obținerea efectivă fiind realizată prin obiectul sys, și el furnizat constructorului, cu titlu obligatoriu.
Pe lângă rolul în obținerea informațiilor de limbă, obiectul sys are și funcția de interfață de acces către itemii de procesat, într-un format anume, întrucât în procesul de interfațare cu procesorul propriu-zis se folosesc doar identificatori. Toate micile asepcte enumerate, inclusiv cel din urmă, în mare nu lipsite de logică, introduc un oarecare cuplaj temporal ce necesită extragerea operațiunilor dorite într-o clasă de natură a le masca și oferi o mai mare ușurință în utilizare.
O ultimă mențiune, probabil inutilă, este că, odată cu actualizarea procesorului trebuie actualizate și fișierele suport (stiluri și localizări) și viceversa. În caz contrar, there will be trouble. Credeți-mă pe cuvânt, am încercat.
Câteva notițe de implementare
Prima problemă de rezolvat este servirea identificatorilor de limbă disponibili, precum și a stilurilor CSL existente. Prima necesită simpla parcurgere a fișierelor de localizare și extragerea acestora din numele fișierului (ex. locales-ro-RO.xml), căci, având în vedere că sunt standard, se pot furniza etichete de către aplicația consumatoare, nemaifiind necesară procesarea fișierelor XML.
Pe de altă parte, lista de stiluri, cu potențial mai ridicat de-a varia… necesită măcar obținerea denumirii din fiecare fișier în parte. Din păcate, dimensiunea fișierului este substanțială (oriunde între 200 și peste 2000 de linii) și nu merită a fi cu totul încărcat în memorie doar pentru o etichetă. Astfel… deși pare complicat, am folosit un procesor SAX pentru acest scop, oprindu-mă când dau de nodul title.
Însă, când vine momentul procesării propriu-zise, citirea și analizarea celor două fișiere devine inevitabilă. Dat fiind tiparul de utilizare, a fost deopotrivă justificabilă și oportună salvarea oarecum agresivă în cache a:
– conținutului brut al fișierelor (folosesc un cache LRU, implementat de-o librărie terță);
– instanțelor de citeproc-js, câte una per perche stil-localizare (folosind același cache LRU).
Evident că abordarea nu este thread-safe, însă, nerulând într-un mediu cu execuție concurentă… nu a cauzat vreun soi de problemă, cu toate că reutilizarea unei instanțe de citeproc-js înseamnă implicit și utilizarea instanței aferente de obiect sys, reclamând inițializare și „curățare” corectă înainte, respectiv după executarea operațiunii dorite.
Așadar, am izolat operarea citeproc-js într-o clasă dedicată generării de citări (generateCitation()), respectiv de bibliografii (generateBibliography()). Pregătește datele de intrare în formatul cerut, actualizează obiectul sys, resetează totul la finalul procesării și convertește rezultatele brute în formatul expus public.
Practic, se asigură că starea internă a citeproc-js, precum și a obiectului sys este gestionată unitar și din exterior fiecare apel are măcar o aparență de atomicitate. Utilizarea necesită doar o singură linie de cod (linia 5, plasată în context):
function generateBibliography(items, style, locale) {
return new Promise((resolve, reject) => {
_createEngine(style, locale)
.then(engine => {
const bibliography = engine.generateBibliography(items);
resolve(bibliography);
})
.catch(engineError => {
reject(engineError);
});
});
}
Conversia propriu-zisă dintre ce se trimite prin punctele de acces și formatul intern cerut de citeproc-js este efectuată la granița sistemului, practic imediat ce-i primită o cerere din exterior. Cei interesați pot consulta funcțiile de conversie aici, nu cred că merită cine știe ce discuție în plus.
În sfârșit, am menționat că formatul HTML produs de procesor a suferit câteva modificări pentru a se preta mai bine conversiei într-un document DOCX. Nu-mi aduc aminte unde este documentată posibilitatea de modificare a formatului, probabil că am parcurs codul librăriei pentru a găsi ce și cum trebuie implementat. În orice caz, rezultatul este aici. Și aici, o încercare pur didactică de XML. Pentru a-l înregistra:
if (CSL.Output.Formats.html != HTML) {
CSL.Output.Formats.html = HTML;
}
iar pentru a instrui procesorul să-l folosească:
engine.setOutputFormat('html');
Atunci când am scris inițial acest server toate operațiunile asincrone foloseau paradigma callback-urilor. M-am gândit că n-ar strica o reîmprospătare și am trecut aproape tot pe promise-uri. Îmbunătățirea de lizibilitate este simțitoare. Ca parte a aceluiași efort, am extras și câteva funcționalități în fișiere separate (cum ar fi rutinele de conversie amintite anterior).
Configurarea server-ului
Fișierul de configurare a aplicației propriu-zise este ./engine/config.js și controlează câteva aspecte precum: dimensiunea cache-ului pentru fișierele de stil și localizare, respectiv pentru instanțele de procesoare, localizările numitelor fișiere de stil și localizare, stilul și localizarea implicite etc.
Opțiune | Tip | Descriere |
---|---|---|
engine.cache.stylesSize | int | Câte fișiere de stil sunt păstrate-n cache |
engine.cache.localesSize | int | Câte fișiere de limbă sunt păstrate-n cache |
engine.cache.enginesSize | int | Câte instanțe de procesor sunt păstrate-n cache |
csl.defsPath | string | Directorul în care sunt stocate fișierele CSL. Implicit ./data/csl-defs |
csl.localesPath | string | Directorul în care sunt stocate fișierele CSL. Implicit ./data/csl-locales |
defaults.style | string | Stilul implicit (dacă nu e specificat de apelant). Valoare curentă: apa. |
defaults.locale | string | Stilul implicit (dacă nu e specificat de apelant). Valoare curentă: en-us. |
debug.port | int | Portul folosit dacă process.env.PORT nu conține nicio valoare. Valoare curentă: 80. |
apiKey | string | Cheia folosită pentru autorizarea apelurilor la server |
Codul sursă
Așa cum v-am obișnuit, codul sursă este pe github. Nu intenționez să dezvolt proiectul în afara demersului punctual descris aici, întrucât momentan nu mai am vreun uz pentru dânsul, reprezintă un artefact pur sentimental. Licența sub care-i publicat – CPAL-1.0 – este determinată de cerințele libăriei citeproc-js.
Alte elemente de interes:
– stilurile CSL incluse în proiect sunt parte a proiectului dedicat de aici;
– localizările incluse în proiect sunt parte a altui proiect dedicat, aici;
– exemple de cereri acceptate de server, aici.
În loc de încheiere…
…O mică istorioară cu iz ironic. Posibilitatea oferită la vremea respectivă utilizatorilor de-a selecta text din documente (cam cum ai colora cu marker-ul pe-o copie fizică ori conspecta de mână-ntr-un carnet) și-a exporta apoi selecțiile folosind cele povestite aici s-a-ntors foarte rapid împotriva noastră din cauză că nu impusesem o limită.
Astfel, utilizatorii mai creativi au selectat și exportat lucrări întregi. Pe de o parte, ne-a bucurat, având confirmarea că sistemul merge brici și cu volume mari. Pe de altă parte… s-a iscat un oarecare scandal intern. Poate e cazul să menționez că biblioteca oferea exclusiv conținut legislativ, clienții fiind… avocați, juriști, studenți la drept etc.
Unul din cei mai prolifici subtilizatori era chiar o avocată, nu cunosc despre competențele ei, însă era mare guristă pe online. A fost ușor să-i detectăm, întrucât totul era jurnalizat, având în vedere că făcea parte din capabilitățile abonamentului și, cu totul rizibil, pe sistem cea mai bună apărare este atacul… nu doar că au negat, ci-au și… amenințat cu proces.
Editura parteneră a ales să nu insiste pe chestiune, însă până la urmă am decis să limităm capabilitățile de conspectare la 5% din conținutul unui document.