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

Aplicaţii Web cu Ymacs, "in-browser Emacs-like editor"

DynarchLIB | Emacs | PHP | javaScript
2009 nov

Ymacs.zip furnizează codul aplicaţiei prezentate aici (corelând cu Ymacs.org). Pentru folosirea aplicaţiei pe Windows, recomandăm instalarea XAMPP (server Apache, PHP, MySQL).

Ordine şi dezordine, în dezvoltarea unei aplicaţii

O aplicaţie implică - la nivel de cod sursă - mai multe fişiere, dependente cumva între ele. Cum scrii fişierele sursă care împreună, produc aplicaţia? Bine - folosind un "editor de text" (fie şi Notepad), sau editorul încorporat într-un IDE; dar… întrebarea rămâne.

Scrii poate - ordonat, "ca la carte": întâi cutare fişier (îl salvezi şi îl închizi - ai terminat cu el), apoi cutare, ş.a.m.d.; când l-ai scris şi pe ultimul atunci ai şi încheiat aplicaţia (mai rămâne să compilezi şi să execuţi, după caz). Bine… să subliniem: aici, "a scrie" nu înseamnă "a copia".

Poate tot aşa scrii o poezie? strofă după strofă, fără "să-ţi baţi capul" şi închei cu semnătura… Un manual te poate instrui să înţelegi o poezie (ar fi şi asta destul), dar parcă nu şi cum să scrii poezie.

În vederea prezentării lucrurilor, manualele asumă ordinea topologică drept principiul esenţial şi indiscutabil… Tocmai de aceea, manualele nu produc niciodată vreo aplicaţie; cel mult, listează undeva la sfârşit fişierele sursă, frumos aşezate unul sub celălalt - lăsând impresia că aşa "se scrie": fişier după fişier, tot înainte.

În realitate, pentru conceperea şi dezvoltarea aplicaţiei dezordinea este motorul lucrurilor; dezordinea este în acest caz un nivel superior al ordinii lucrurilor - acel nivel care ştie cum sunt corelate lucrurile pe care manualul doar le secvenţiază până la separare, nemaireuşind să le integreze cum se cuvine.

Pentru o aplicaţie Web sunt de scris mai multe "secţiuni" de diverse tipuri (HTML, CSS, javaScript, PHP, Perl, etc.) şi de obicei, editarea fişierelor respective nu decurge în mod liniar, ci mai degrabă intercalat, corelativ, oarecum "simultan", progresiv dar adesea şi regresiv. Justificarea minimă pentru dezordinea subiacentă acestei "tehnici" de lucru este dată de necesitatea firească de a testa aplicaţia (deschizând-o în browser) în diverse stadii de lucru, ca şi de faptul că HTML, CSS, javaScript sunt complementare şi inseparabile în cadrul unei aplicaţii Web (în timp ce manualele separă, dar permanent, lucrurile: de aici până aici e "limbajul C++" care nu are de-a face cu alte limbaje afară de… "pseudocod", de aici până aici e "HTML", urmează "CSS", etc.).

Edităm, prezentăm, facem… Cum adică?

Având de editat mai multe fişiere, poate şi voluminoase, cu tag-uri de marcare specifice, cu numeroase acolade, paranteze, ghilimele şi apostrofuri - nu mai ajunge suportul oferit de un "editor de text" obişnuit. Se foloseşte un editor de cod-sursă, cu facilităţi de evidenţiere sintactică pentru diverse limbaje, indentare automată, etc.; Emacs apărut în 1976 (dezvoltat continuu) este cel mai complex produs de acest tip şi nu degeaba este el încorporat în toate distribuţiile majore de Linux (producându-se versiuni şi pentru Windows).

Însă în contextul şcolar… lucrurile se complică: profesorul nu are de creat cine ştie ce aplicaţie, cât mai ales de a prezenta şi demonstra altora tehnici, cunoştinţe şi principii elementare de construire a unei aplicaţii - în mod practic, cu experimente şi lămuriri sau atenţionări intermediare. Una este să "prezinţi" aplicaţia gata făcută şi cu totul altceva, să o faci "pe viu" - arătând cum se construieşte şi cum se asamblează ea, pas cu pas.

"Lucrurile se complică" este o sintagmă subtilă… dar ne-am tot referit pe aici, la alienările învăţământului faţă de informatică - avem un sistem închis, bazat exclusiv pe Windows, pe produse Microsoft şi pe "point-and-click". Deschiderea posibilă ar putea consta în valorificarea unui browser modern (Firefox, fără îndoială) - cu atât mai mult cu cât tendinţa este aceea de a putea face mai toate cele folosind numai un browser!

Avem deja biblioteci javaScript de tip in-browser code editing, competente - anume CodeMirror şi Ymacs; cu anumite extinderi, ele ar permite prezentarea cursivă a dezvoltării pas cu pas a unei aplicaţii, implicând pentru aceasta numai browserul (şi eventual, un server).

CodeMirror permite între altele, ideea de manual interactiv - de exemplu, Eloquent JavaScript, din care reproducem (din browser) această imagine (text plus consolă de lucru):

Editezi în consola oferită codul tocmai explicat şi observi imediat şi rezultatele; editezi (direct, fără să "ieşi"), faci propriile exemple şi sigur, înţelegi mai bine şi mai lesne lucrurile…

Pe de altă parte, Ymacs modelează (în javaScript) arhitectura şi unele uzanţe fundamentale din Emacs; vom arăta mai departe cum îl putem folosi în scopul didactic descris mai sus - ceea ce sintetizăm acum prin:

Am deschis în Firefox Ymacs (pentru care am înfiinţat un "virtual host", http://ymacs.d); am constituit două buffere - într-unul edităm cmmdc.html, în celălalt gcd.js; în tab-ul Firefox vecin, putem testa aplicaţia constituită din fişierele editate în bufferele curente din Ymacs. Desigur, dezvoltăm aplicaţia "pas cu pas", începând cu elementele funcţionale esenţiale, salvăm bufferele, testăm rezultatul, reluăm apoi (după caz) editarea în Ymacs pentru a completa corespunzător fişierele componente ale aplicaţiei. Totul, fără a ieşi din browser!

Constituirea unui web-server pentru a folosi Ymacs

Descărcăm ymacs-latest.tar.gz şi dezarhivăm, obţinând o anumită structură de fişiere, cu rădăcina Ymacs. Putem încărca în browser fişierul Ymacs/test/index.html (prin protocolul file:///):

Iniţial, aveam un singur cadru, care conţinea unul dintre bufferele prestabilite; dar am folosit (ca şi în Emacs) combinaţia de taste C-x 1 (CTRL şi x, apoi tasta 1) pentru a obţine două cadre alăturate pe orizontală; am folosit apoi C-TAB, în fiecare cadru, pentru a încărca un altul dintre bufferele existente. Am deschis apoi, meniul "Load its own code!", cum se vede pe reproducerea de mai sus…

Fişierele indicate în acest meniu (pe imagine e marcat ymacs.js - am vrut chipurile, să-l încărcăm în buffer) se găsesc în Ymacs/src, dar nu pot fi încărcate - fireşte: în general, din javaScript nu se pot accesa fişiere de pe disc. Demonstraţia produsă la http://www.ymacs.org/demo/ se bazează pe exact aceleaşi fişiere pe care le-am obţinut în directorul propriu Ymacs (a vedea sursa "demo" şi a compara cu fişierul Ymacs/test/index.html pe care noi l-am deschis prin file:///) - dar acolo meniul funcţionează. Diferenţa este desigur, aceea dintre http:// şi file:///; prin "http" se pot obţine fişiere dintr-un anumit director.

Prin urmare, pentru a folosi Ymacs în sensul specificat (operând cu "Load/Save") trebuie întâi să creem un virtual host care să conţină Ymacs. Pentru Windows, se poate obţine Apache, MySQL, PHP (şi phpMyAdmin, etc.) de la XAMPP; presupunem că s-a instalat XAMPP şi avem C:\xampp\.

Pentru a crea un "virtual host", întâi definim un nume de domeniu astfel: deschidem fişierul C:\windows\system32\drivers\etc\hosts, în care adăugăm o linie de forma: 127.0.0.1 ymacs.d; salvăm, lansăm Apache şi putem proba din browser că domeniul e recunoscut: http://ymacs.d ar trebui să dea acelaşi rezultat ca http://localhost (fiindcă domeniul a fost doar declarat şi nu are încă, alte specificaţii).

Pentru ca http://ymacs.d să furnizeze fişiere din directorul nostru Ymacs/, trebuie să facem anumite configurări prealabile. În C:\xampp\apache\conf\httpd.conf găsim definiţia DocumentRoot /xampp/htdocs - implicând că vor putea fi servite documentele existente în directorul /htdocs (numai acestea, cât timp configurările nu sunt particularizate ulterior). Pentru a proceda probabil cel mai simplu, mutăm Ymacs ca subdirector în htdocs; deschidem apoi fişierul /xampp/apache/conf/extra/httpd-vhosts.conf şi adăugăm:

     NameVirtualHost 127.0.0.1:80   # iniţial, această linie era marcată "comentariu"
     <VirtualHost ymacs.d>
         DocumentRoot "C:/xampp/htdocs/Ymacs"
         ServerName ymacs.d
     </VirtualHost>

În final, stopăm şi restartăm Apache, pentru a activa configurarea făcută. Putem proba acum http://ymacs.d/test (în mod uzual, "index.html" este fişierul implicit încărcat, când este găsit în directorul respectiv):

Să ne amintim că iniţial - prin file:///...Ymacs/test/index.html - nu puteam încărca fişiere din meniul "Load its own code"; acum aceasta a devenit posibil, cum se vede în imaginea reprodusă mai sus (preluată după încărcarea în buffer a fişierului ymacs.js).

Iluminări…

Să ne mai amintim şi de intenţia noastră de a folosi practic Ymacs, în scop didactic: pentru a contura pas cu pas mici aplicaţii Web (sau a le prezenta şi a experimenta) lucrând numai în browser. Astfel că vom eluda aspectele demonstrative, metodice şi instructive proprii pachetului original (descărcat de la Ymacs.org), permiţându-ne unele simplificări şi reorganizări.

Ymacs oferă şi o posibilitate de a "salva/încărca": selectăm în bufferul curent ceea ce vrem să "salvăm" şi folosim combinaţia de taste M-S-w (unde M desemnează tasta Alt şi S tasta Shift) pentru a înscrie conţinutul respectiv în "clipboardul sistemului"; apoi (minimizând eventual instanţa curentă a browserului), deschidem fişierul în care voiam să pastăm conţinutul selectat din bufferul Ymacs (şi operăm invers pentru a "încărca" dintr-un fişier de pe disc, pastând clipboardul în buffer prin C-S-y).

Desigur că acest procedeu nu ne convine pentru ceea ce ne-am propus mai sus - ar trebui iarăşi să minimizăm ferestre, să lucrăm intermediar iarăşi cu Notepad, etc. Alegem să rezolvăm chestiunea prin două funcţii PHP, una pentru "Load" şi una pentru "Save" - încât să nu mai fim obligaţi să "ieşim" din browser pe parcursul dezvoltării aplicaţiei în bufferele Ymacs; desigur că aceste funcţii vor trebui să fie conectate de undeva din pagina de bază index.html şi rămâne de văzut cum facem aceasta.

Pagina index.html originală încarcă între altele, fişierele din Ymacs/src/js; acestea definesc şi integrează obiectele javaScript necesare pentru gestionarea bufferelor şi cadrelor de fereastră, definind comportarea cursorului şi anumite comenzi (mapări între combinaţii de taste şi anumite funcţii), precum şi câteva "moduri de lucru" (pentru javaScript, XHTML, lisp). Desigur, fişierele sursă ("în clar") servesc dezvoltatorilor - de exemplu pentru a adăuga un mod de lucru pentru fişiere PHP trebuie investigate fişierele sursă existente.

În cazul nostru însă, putem omite subdirectorul src/js; păstrăm (şi încărcăm prin index.html) numai fişierul src/js-minified/ymacs-min.js, care concatenează şi comprimă fişierele din src/js.

În finalul său, index.html încarcă Ymacs/test/test.js - fişier care creează interfaţa specifică, angajând pentru aceasta biblioteca-toolkit DynarchLIB. Aici putem vedea, cum se procedează pentru a încărca un buffer cu un anumit conţinut, inclusiv pentru a încărca într-un buffer conţinutul unuia dintre fişierele din meniul "Load its own code" - deducând eventual cum să conectăm funcţiile PHP intenţionate de noi pentru "Load" şi "Save".

"Load/Save", cu DynarchLIB RPC() şi PHP

Folosind documentaţia de la DynarchLIB (dar şi sprijinul direct al autorului), am modificat procedura de încărcare în buffer a unui fişier (existentă în test.js original) astfel încât să se apeleze întâi funcţia yload.php; aceasta determină şi returnează lista fişierelor existente în Ymacs/devapp (subdirector constituit în scopul de a păstra fişierele editate cu Ymacs).

var menu = new DlHMenu({}); // bară de meniu orizontală; v. widgets-menu
menu.setStyle({ marginLeft: 0, marginRight: 0 });

// opţiunea "Load" va lista fişierele dintre care se va selecta unul, 
//                                     de încărcat în bufferul curent
var item = new DlMenuItem({ parent: menu, label: "Load ".makeLabel() }); 
var files = []; // conţinutul subdirectorului Ymacs/devapp 
                // (va fi obţinut de la yload.php)
item.setMenu(function(){
    var menu = new DlVMenu({}); // creează un meniu vertical
        
    // iniţiază un apel RPC "remote procedure call", 
    // obţinând de la funcţia yload.php şi înscriind în meniul vertical creat,
    // lista fişierelor din Ymacs/devapp (v. şi DlRPC)
            
    new DlRPC({ url: "/yphp/yload.php", method: "POST" })
                                        .call({ callback: function(data){
        menu.destroyChildWidgets(); 
        data.text.split(/\s+/).sort().foreach(function(file){
            var item = new DlMenuItem({ label: file, parent: menu });
            item.addEventListener("onSelect", function(){
                // la click pe un item din listă, se va obţine prin DlRPC() 
                // conţinutul actual al fişierului
                var request = new DlRPC({ url: "../../devapp/" + file + 
                                               "?killCache=" + new Date().getTime() });
                request.call({
                    callback: function(data){
                        var code = data.text;
                        var buf = ymacs.getBuffer(file) || 
                                  ymacs.createBuffer({ name: file });
                        buf.setCode(code);
                        // asociază bufferului un "mod de lucru"
                        // pentru PHP trebuie creat unul, dar e bun şi "xml_mode" 
                        if(/(html|css|php)$/.test(file)) buf.cmd("xml_mode", true);
                        else buf.cmd("javascript_dl_mode", true);
                            
                        ymacs.switchToBuffer(buf);
                    }
                });
            });
        });
    }});
    return menu;
});

Pentru fiecare item din meniul constituit de lista de nume de fişiere întoarsă de yload.php, s-a asociat evenimentului onSelect corespunzător acelui item, o funcţie care va cere serverului (iarăşi prin RPC) conţinutul fişierului - transmiţând însă nu doar numele acestuia, ci şi un parametru killCache având ca valoare timpul curent al cererii; în acest fel, se evită ca browserul să accepte drept "răspuns" o copie mai veche a fişierului (prezentă în "Cache"), în loc de a forţa serverul să (re)transmită conţinutul actual al fişierului.

Redăm şi fişierul yload.php - dar nu este decât maniera tipică în PHP (v. totuşi PHP-manual) pentru a crea un handler de director şi a-i citi intrările existente:

<?php
$xdir = '../devapp/';  // aici trebuie puse fişierele de editat
$xext = array('html', 'js', 'css', 'php', 'cpp', 'txt');
$xlist = '';  // lista fişierelor (am preferat ca şir de nume separate prin spaţiu)
if(file_exists($xdir)) {
    $xd = opendir($xdir) or die('cannnot open director');
    while($xfile = readdir($xd)) {
        if($xfile != '.' && $xfile != '..') {
            $xinfo = pathinfo($xfile);   // var_dump($xinfo);
            if(in_array($xinfo['extension'], $xext)) {
                $xlist .= $xfile . ' '; // adaugă numele în listă
            }
        }
    }
    closedir($xd);
}
echo $xlist;
?>

Pentru operaţia "Save" am procedat analog cazului "Load" redat mai sus; se creează un obiect DlRPC() prin intermediul căruia se transmit funcţiei ysave.php numele fişierului (cerut prin prompt(), implicit fiind numele bufferului) şi conţinutul bufferului, urmând ca ysave.php să scrie fişierul în directorul ştiut:

    var item = new DlMenuItem({ parent: menu, label: "Save".makeLabel() });
    item.addEventListener("onSelect", function() {
        var rpc = new DlRPC({
            url: "/yphp/ysave.php",
            data: {
                name:  prompt("Nume: ", ymacs.getActiveBuffer().name),
                content: ymacs.getActiveBuffer().getCode()
            }
        });
        // alert(rpc.data['name'] + '\n' + rpc.data['content']);
        rpc.call();
    });

Următoarea imagine, de sub Firebug, arată că s-a contactat ysave.php, transmiţându-i prin metoda POST (implicită în DlRPC(), în cazul de faţă) un obiect javaScript codificat JSON, {"name": "valoare", "content": "valoare"} (acesta va trebui să fie decodificat şi "rescris" în PHP de către ysave.php):

Ne-am referit deja la conversia unui obiect javaScript în şir JSON şi reconversia acestuia într-o variabilă Perl - v. Integrarea algoritmului lui Dijkstra. Analog avem de procedat şi în PHP; în fişierul ysave.php se preia de pe stream-ul de intrare şirul JSON transmis de DlRPC()-ul redat mai sus şi i se aplică json_decode(), rezultând variabila PHP numită mai jos $data; aceasta este o referinţă la tabloul asociativ de chei "name" şi "content" şi valorile acestora servesc apoi pentru scrierea în fişier cerută:

<?php
$str = file_get_contents("php://input"); // v. PHP input/output streams
$data = json_decode($str); 

$fname = $data->{"name"};
$finame = "../devapp/$fname";
$content = $data->{"content"};

$handle = fopen($finame, 'wb'); 
fwrite($handle, $content); 
fclose($handle);
?>

Obs. Desigur, pentru ca "Save" să şi funcţioneze, trebuie să mai avem în vedere că pe lângă "nume" şi "conţinut" (explicit considerate mai sus), un fişier are şi alte atribute, cu efect implicit asupra posibilităţii operaţiilor de citire/scriere (v. permissions); /devapp trebuie setat astfel încât utilizatorii să aibă dreptul de a citi şi de a scrie fişiere în acest subdirector.

vezi Cărţile mele (de programare)

docerpro | Prev | Next