<?php
// parser za sadržaj stranice, pronalazi kodove oblika \[[-a-zA-Z0-9._]+(\s<parametri>)?\]
/* Spisak jednostavnih oznaka, ostale su složene, tj. potrebna je
posebna funkcija za njihovu obradu
*/
$sve_oznake = array (
"b" => array('<em class="bold">','</em>'),
"i" => array('<em class="kurziv">','</em>'),
"crveno" => array('<em class="crveno">','</em>'),
"naslov" => array('<h1>','</h1>'),
"podnaslov" => array('<h2>','</h2>'),
"podpodnaslov" => array('<h3>','</h3>'),
"linija" => "<hr>",
"lista" => array("<ul>","</ul>"),
"stavka" => array("<li>","</li>"),
"email" => array("<a href=\"mailto:[adresa]\">","</a>"),
);
/*
Obrađuje pojedinačnu oznaku, tj. sve što se nalazi između "[" i "]".
Znak na $txt[$poz-1] treba da bude '[', a vraća se pozicija ']'.
*/
function obradi_oznaku(&$txt,$poz=0) {
$oznaka='';
$origstr='';
while (dozvoljeno_u_oznaci($txt[$poz])) {
$origstr .= $txt[$poz];
$oznaka .= $txt[$poz++];
}
$atributi=array();
while ($poz<strlen($txt) and $txt[$poz]!=']') {
if (razmak($txt[$poz])) {
$origstr .= $txt[$poz];
$poz++;
} elseif (dozvoljeno_u_oznaci($txt[$poz])) {
$ime_atr='';
while ($poz<strlen($txt) and dozvoljeno_u_oznaci($txt[$poz])) {
$origstr .= $txt[$poz];
$ime_atr .= $txt[$poz++];
}
$vrednost='';
if ($txt[$poz]=='=') {
$origstr .= $txt[$poz];
$poz++;
if ($txt[$poz]=='"' or $txt[$poz]=="'") {
$origstr .= $txt[$poz];
$vrednost='';
$kraj=$txt[$poz]; $poz++;
while ($poz<strlen($txt) and $txt[$poz]!=$kraj) {
$origstr .= $txt[$poz];
if ($txt[$poz]=='\\' and $txt[$poz+1]==$kraj) {
$vrednost .= $kraj;
$poz++;
$origstr .= $txt[$poz];
} else $vrednost .= $txt[$poz];
$poz++;
}
} else {
$vrednost='';
while (dozvoljeno_u_oznaci($txt[$poz])) {
$origstr .= $txt[$poz];
$vrednost .= $txt[$poz++];
}
$poz--;
$origstr=substr($origstr,0,-1);
}
} else {
$poz--;
$origstr=substr($origstr,0,-1);
}
$atributi[$ime_atr]=$vrednost;
$origstr .= $txt[$poz];
$poz++;
} else {
$origstr .= $txt[$poz];
$poz++;
}
}
$atributi['orig_str']=$origstr;
return array($oznaka,$atributi);
}
function dozvoljeno_u_oznaci($slovo) {
return ereg("^[-A-Za-z0-9._]$",$slovo);
}
function razmak($slovo) {
return (($slovo==' ') or ($slovo=="\t") or ($slovo=="\n") or ($slovo=="\r"));
}
function tekst_oznake($oznaka, $sadrzaj, $atributi=array()) {
global $sve_oznake;
if (is_array($sve_oznake[$oznaka])) {
$txt = $sve_oznake[$oznaka][0].$sadrzaj.$sve_oznake[$oznaka][1];
if (is_array($atributi))
foreach (($atributi) as $atr => $vrednost) {
if ($atr != 'orig_str') $txt = str_replace("[$atr]",$vrednost,$txt);
}
return $txt;
} elseif (is_string($sve_oznake[$oznaka])) {
return $sve_oznake[$oznaka].$sadrzaj;
} else {
// ovde ne bi trebalo nikad da stigne, ali za svaki slučaj
return "[$oznaka]$sadrzaj"."[/$oznaka]";
}
}
/*
Jedna oznaka u nizu $stanje je zapravo par koji sadrži ime oznake i
njene osobine (atribute). Osobine su jedan rečnik u kom su ključevi
imena osobina, a vrednosti njihove vrednosti. Naročita osobina sa
imenom "orig_str" sadrži pun tekst same oznake (sve između "[","]").
*/
function obradi_tekst($txt,$stanje=array(),$poz=0,$adresa='',$jezik='en') {
global $sve_oznake;
$sadrzaj='';
while ($poz<strlen($txt)) {
if ($txt[$poz]=='[') {
if ($txt[$poz+1]=='/') {
$poz=$poz+2;
$zatvaranje='';
while (dozvoljeno_u_oznaci($txt[$poz]) and $poz<strlen($txt)) {
$zatvaranje .= $txt[$poz++];
}
$ostatak=''; // sve do ']' je nebitno
while ($txt[$poz] != ']' and $poz<strlen($txt)) $ostatak.=$txt[$poz++];
$poslednji = array_pop($stanje);
if ($poslednji and $zatvaranje == $poslednji[0]) {
return tekst_oznake($zatvaranje,$sadrzaj,$poslednji[1]);
} else {
$sadrzaj.="[/$zatvaranje$ostatak]";
$stanje[]=$poslednji;
}
} else {
$poz=$poz+1;
list($imeoznake,$argumenti)=obradi_oznaku(&$txt,&$poz);
$argumenti['trenutno']=$adresa;
$argumenti['jezik']=$jezik;
$stanje[]=array($imeoznake,$argumenti);
$poz++;
if (is_array($sve_oznake[$imeoznake])) {
// ovde treba obradi_tekst do zavrsne oznake
$sadrzaj.=obradi_tekst(&$txt,$stanje,&$poz,$adresa,$jezik);
array_pop($stanje);
} elseif (is_string($sve_oznake[$imeoznake])) {
$sadrzaj.=tekst_oznake($imeoznake,'',$argumenti);
array_pop($stanje);
$poz--;
} else {
// inace treba pozvati funkciju!!!
$proba=pozovi_oznaku($txt,&$poz,&$stanje,$imeoznake,$argumenti);
if ($proba) {
$sadrzaj.=$proba;
$poz--;
} else {
$sadrzaj .= "[$argumenti[orig_str]]"; $poz--;
}
array_pop($stanje);
}
}
} else {
if (($txt[$poz]=="\n")and (ord($txt[$poz+1])==13)) {
$sadrzaj.="\n\n<p>";
$poz++;
} else {
$sadrzaj.=htmlspecialchars($txt[$poz]);
}
}
$poz++;
}
return $sadrzaj;
}
function pozovi_oznaku($txt,$poz,$stanje,$imeoznake,$argumenti) {
global $putanja_oznaka;
$prom="ucitao_oznaku_za_$imeoznake";
global $$prom;
$trazifajl="$putanja_oznaka/$imeoznake.php";
if (! file_exists($trazifajl)) {
return FALSE;
} else {
if ($$prom != 1) {
include ($trazifajl);
}
$fja="obradi_oznaku_$imeoznake";
$txt=$fja(&$txt,&$poz,&$stanje,$argumenti);
return $txt;
}
}
?>
Par napomena:
— $sve_oznake sadrži rečnik koji „mapira“ oznake (tagove) na ono što ta oznaka treba da ubaci. Ukoliko pokazuje na string, onda se radi o „nezatvorenom“ tagu (nešto tipa SGML SHORTTAG). Ukoliko pokazuje na dvočlani niz, onda se prvi član koristi za početak, a drugi za kraj, i uz to se [ime_atributa] zamenjuje sa vrednošću atributa
— ukoliko oznaka ne postoji u $sve_oznake, onda se njena definicija pokušava naći u fajlu „$putanja_oznaka/$ime_oznake.php“ i to kao f-ja sa imenom „obradi_oznaku_$ime_oznake“ (ako fajl postoji, a ne postoji ovakva funkcija, onda dolazi do sintaksne greške; ako neko zna kako ovo srediti, neka javi)
— osnovna funkcija je „obradi_tekst“ kojoj se prosleđuje ceo tekst koji treba obraditi (poželjno prosleđivati referencu, pošto se taj tekst ne menja), tekuće stanje parsera (pri prvom pozivanju, samo prosledite prazan niz; koristi se kao stek) i pozicija u tekstu. Ona se poziva rekurzivno čim se naiđe na neku oznaku, ali kako se pozicija prosleđuje kao referenca, to se nastavlja posle te oznake (tj. ukupno jedan prolaz kroz ceo tekst)
neki parametri ove funkcije su nepotrebni za sam parser, ali za konkretan posao koji sam radio su mi bili potrebni (jezik, trenutna adresa, itd.) izbacivanjem ovoga, parser će biti samo efikasniji, i trošiće manje memorije
— takođe postoji i naročito procesiranje „praznog reda“ (dva \n\n), kada se ubacuje oznaka za novi pasus — ovo je lako ukloniti prema potrebama
— oznake se sastoje od znakova koji se nalaze u „dozvoljeno_u_oznaci“ funkciji; prema tome, ako želite da proširite, samo izvolite
— jedna oznaka je oblika:
OZNAKA := "[" <KLJUČNA_REČ> ([^<oznaka>]<parametar>) "]"
KLJUČNA_REČ := [<oznaka>]+
<oznaka> je spisak znakova dozvoljenih u oznaci
<parametar> := [<oznaka>]+ "=" <vrednost>
<vrednost> := KLJUČNA_REČ | "'" [^']* "'" | "\"" [^"]* "\""
npr. [link broj=456 ref=pera-14 strana="eto ti ga na" odakle='sta ti "oces" []bre']
završne oznake su samo oblika "[/" <KLJUČNA_REČ> .* "]" (znači npr. „[/link ovo posle nije bitno]“).
Evo i par primera oznaka pomoću funkcija.
Fajl oznake/html.php (ubacuje neprocesirani kod):
<?php
function obradi_oznaku_html($txt,$poz,$stanje,$argumenti) {
global $ucitao_oznaku_za_html;
$ucitao_oznaku_za_html=1;
$rez='';
while (substr($txt,$poz,strlen('[/html]')) != '[/html]' and $poz<strlen($txt)) {
$rez.=$txt[$poz];
$poz++;
}
if (substr($txt,$poz,strlen('[/html]')) == '[/html]') $poz+=strlen('[/html]');
return "$rez";
}
?>
Ovo je vrlo jednostavan primer ubacivanja „neprocesiranog“ koda.
Drugi primer je kada imamo veze na spoljašnje stranice (pa treba dodati „http://“ ako protokol nije definisan); fajl oznake/url.php:
<?php
function obradi_oznaku_url($txt,$poz,$stanje,$argumenti) {
global $ucitao_oznaku_za_url;
$ucitao_oznaku_za_url=1;
$rez='';
while (substr($txt,$poz,strlen('[/ url]')) != '[/ url]' and $poz<strlen($txt)) {
$rez.=$txt[$poz];
$poz++;
}
if (substr($txt,$poz,strlen('[/ url]')) == '[/ url]') $poz+=strlen('[/ url]');
$rez=obradi_tekst($rez,$stanje,0);
if (ereg("^[A-Za-z]+:",$argumenti[adresa])) { // znači, već je dat „protokol“
return "<a href=\"$argumenti[adresa]\">".$rez."</a>";
} else {
return "<a href=\"http://$argumenti[adresa]\">".$rez."</a>";
}
}
?>
I ovo je jednostavno, a pokazuje i kako se ugnježdeni tekst obrađuje po istim pravilima (samo se pozove „obradi_tekst()“). Naravno, ovakva oznaka „url“ se ne može koristiti za unutrašnje relativne linkove, ali to je i namerno (ja uglavnom napravim novu oznaku za unutrašnje linkove koja proverava da li taj link zaista postoji [tome i služe oni parametri „adresa“ i „jezik“ u obradi_tekst()], i na taj način izbegavam pogrešne unutrašnje linkove).
— oznake za tagove kakvi se koriste na ES-u ([oznaka=www.adresa.com]nešto[/oznaka]) se lako mogu napraviti korišćenjem ovih funkcija i atributa „orig_str“.
— ono što bi bilo lako napraviti je prosleđivanje reference na spisak oznaka u obradi_tekst, umesto upotrebe globalnih $sve_oznake. Tada bi bilo moguće definisati strožija pravila (tipa XML-a i SGML-a): npr. u oznaku „para“ se mogu ugnjezditi samo oznake „b“, „i“, „kod“, „url“ i još neke, a ne mogu „listing“, „naslov“, itd.
— Licenca za ovo „čudo“ je GNU GPL :-P
Naravno, sve primedbe su dobrodošle, kao i poboljšanja (kako ne poznajem PHP baš najbolje, ima tu mnogo mesta za njih), itd.
U svakom slučaju, ovo bi trebalo da bude znatno brže od regexp parsiranja koje prolazi po nekoliko puta kroz tekst, ali nisam siguran da je tako. Takođe sadrži i gomilu lepih osobina (bar za mene), pa mislim da je koristan.
Naravno, ako nađete grešku („bug“), dolazim da vas bijem, jer ne želim da se naruši moj ugled sveznajućeg (ko čita PHP forum, zna na šta mislim ;-)