Login »
get firefox
articoli
Torna all'elenco degli articoli

Suddividere un testo HTML in pagine con PHP
scritto da ghiaccio il 15/06/2007

La suddivisione in pagine di testi può diventare a volte un punto cruciale nella realizzazione di un sito web.

Non parlo di suddividere gli elementi di una tabella di database che sono già suddivisi in record. In questo caso si può facilmente scegliere di visualizzare un numero arbitrario di tuple (record) per pagine.

Il problema riguarda la suddivisione di testi molto lunghi tipicamente presenti su una tabella di database come campo di una sola riga.
L'amministratore del sito inserisce i testi nel database, solitamente tramite un CMS (Content Management System) e tali testi possono magari essere  articoli tecnici, relazioni, statuti aziendali o quant'altro sia molto lungo in modo tale che la loro visualizzazione in una sola pagina sia quantomeno poco professionale.

Quando poi le esigenze di chi gestisce il sito (i vostri 'clienti') necessitano di poter creare testi formattati, il testo conterrà dei tag HTML (o particolari 'tag' predisposti a questo scopo) e quindi le cose si complicano.

In questo articolo analizzeremo il problema utilizzanndo il popolare linguaggio di scripting lato server PHP.

Per testi privi di tag HTML la soluzione è relativamente semplice ma il concetto è lo stesso.
Decidiamo una lunghezza in termini di numero di caratteri per pagina ed eseguiamo un ciclo che inserisce in un array di stringhe i primi X caratteri del testo iniziale.
Il problema è che dopo gli X caratteri si interrompe sicuramente la pagina in corrispondenda di una frase, o una parola.
Quindi è necessario continuare ad aggiungere testo all'elemento dell'array (della pagina) fino a che si trova un separatore, la soluzione ottimale è il ritorno a capo.
Ecco quindi la versione base della funzione che chiamerò getSubdividedText che prende due argomenti: il testo iniziale e la lunghezza (minima) delle pagine.

   
function getSubdividedText($text,$minPageLen)
    {
         $pageArr=array(); //inizializza
   
        for ($i=0;$text!="";$i++) //suddivisione in pagine
        {
            $pageArr[$i]=substr($text,0,$minPageLen); //prime 500 lettere
            $text=substr($text,$minPageLen); //elimina le prime 500 da text
           
            $brPos=strpos($text,"<br />");
            if ($brPos!==false)                                      //se c'è un separatore
            {    
                $pageArr[$i].=substr($text,0,$brPos+6);          //agginge all'array del testo aggiuntivo fino al separatore successivo
                $text =substr($text,  $brPos+6);                      //ed elimina tale porzione di testo dal testo iniziale rimanente
            }else                                                          //altrimenti senza più separatori
            {
                $pageArr[$i].=substr($text,0);                        //agginge all'array tutto il testo rimanente
                $text ="";                                                       //e il testo rimane vuoto. A questo punto termina il ciclo for
            }

            return $pageArr;
    }


La funzione contiene il ciclo for che continua fino a che l'argomento $text non viene completamente svuotato, l'array ritornato dalla funzione contiene quindi tanti elementi quanti sono i cicli for.

Il corpo del ciclo si divide come descritto sopra in due parti.
1) Immagazzinamento dei primi $minPageLen caratteri nell' i-esimo elemento dell'array ed eliminazione di essi da $text

$pageArr[$i]=substr($text,0,$minPageLen);
$text=substr($text,$minPageLen);


2) Rilevamento della posizione del separatore scelto nella variabile $brPos ed aggiunta nell' i-esimo elemento dell'array del testo fino a separatore (ed eliminazione del testo da $text). Nel caso in cui in $text non ci sia rimasto un separatore, si inserisce nell'array tutto quanto il testo rimanente.


Potreste obiettare che questo procedimento base si riferisce a testi senza tag, però si assume il tag <br /> come separatore di pagine!
Il motivo per si può voler usare il <br /> è che i testi privi di formattazione inseriti tramite una <textarea> HTML, vengono solitamente memorizzati nel database sostituendo le interruzioni di riga \r\n con un <br /> per poi evitare questa sostituzione in fase di ogni visualizzazione del testo.

Nella funzione potete comunque sostituire il separatore <br /> con qualsiasi cosa ad esempio \r\n e sostituire 6(lunghezza stringa '<br />') con la lunghezza del separatore nel terzo parametro delle chiamate alla funzione substr.

Ok, a questo punto consideriamo il problema dei tag presenti nel testo.
Per quello che dobbiamo fare bisogna considerare che i tag (X)HTML si dividono in due categorie, i tag che si autochiudono ed i tag che non si autochiudono.

I tag autochiusi sono tag come br (<br />), hr (<hr />) o img (<img src="..." />) che non hanno un corrispettivo tag di chiusura. Questi tag non ci danno particolari problemi in quanto sono come una frase e non potranno mai essere 'spezzati' dal nostro separatore (non esiste <hr><br /></hr>).

Il problema ce lo danno i tag che non si autochiudono in quanto molto probabilmente la suddivisione in porzioni del testo, non comprenderà la chiusura di alcuni di questi tag.
La soluzione non può essere la ricerca del prossimo eventuale tag di chiusura, in modo simile a come facevamo col separatore, in quanto ci potrebbe essere ad esempio un div che contiene tutto il testo e quindi la suddivisione in pagine non avverrebbe.

La soluzione che ho adottato invece, prevede di ricercare nella porzione di testo appena creata tutti i tag che si aprono e metterli in un array. Se durante la scansione si trovano delle chiusure di tag che erano state messe nell'array, si eliminano tali elementi dall'array.
Alla fine della scansione otteniamo quindi un array che contiene tutti i tag aperti e non chiusi nel testo e si può facilmente aggiungere alla porzione di testo che si è analizzato le chiusure di questi tag.
Viceversa al ciclo che segue quando si crea la porzione di testo successiva, si possono riaprire i tag che abbiamo chiuso manualmente nella pagina precedente e , fatto questo, svuotare l'array per un nuovo utilizzo.

Si potrebbe pensare che questo array implementi uno stack (pila) perchè ci vengono aggiunti i tag e scandendo il testo se si trovano le chiusure si rimuove dalla testa, poi nella pagina successiva si estraggono gli elementi dalla testa (per riaprire i tag) fino a svuotarlo.
Invece non è proprio un stack in quanto l'operazione di estrazione di un tag quando si trova una chiusura, deve avvenire sull'elemento corrispondente che non è (quasi mai) in testa allo stack.
Infatti nel frammento di codice HTML sintatticamente corretto <span><em></span></em> avremmo ad un certo punto lo stack della forma |span|em e trovando poi la prima chiusura di span sarà necessario cercare il tag span da rimuovere nello stack, cominciando a cercare dalla testa (em) sino al primo elemento (span, l'elemento cercato).
Questa è una considerazione abbastanza delicata ed importante per riuscire a mantenere la struttura del codice suddivisa nelle porzioni di testo.

Riassumendo il funzionamento, ad esempio si ha:

Testo completo
<div>
    <span>
       .......Testo 1.......
       .......Testo 2.......
    </span>
</div>

Che diventa

1°pagina
<div>
    <span>
       .......Testo 1.......
    </span>
</div>

2°pagina
<div>
    <span>
       .......Testo 2.......
    </span>
</div>


Passiamo quindi ad esaminare il corpo definitivo della funzione getSubdividedText:

   
function getSubdividedText($text,$minPageLen)
    {
        $autoClosedTag=array('img','hr','br');

           
        $openTags=array(); //inizializza
        $pageArr=array(); //inizializza
           
        for ($i=0;$text!="";$i++) //suddivisione in pagine
        {
            $pageArr[$i]=substr($text,0,$minPageLen); //prime 500 lettere
            $text=substr($text,$minPageLen); //elimina le prime 500 da text
           


       if ( $brPos!==false && trim(strip_tags(substr($text,$brPos+6)))!='') //se c'è un br e il testo seguente non è "vuoto"...
       {
               $pageArr[$i].=substr($text,0,$brPos+6); //agginge all'array del testo aggiuntivo fino al separatore successivo
               $text =substr($text, $brPos+6); //ed elimina tale porzione di testo dal testo iniziale rimanente
       }
       else //...altrimenti senza più separatori OPPURE con testo rimanente costituito da sole chiusure di tag
       {
               $pageArr[$i].=substr($text,0); //agginge all'array tutto il testo rimanente
               $text =""; //e il testo rimane vuoto. A questo punto termina il ciclo for
       }


           foreach ($openTags as $tag)//apertura dei tag precedentemente chiusi
            {
                $pageArr[$i]="<".$tag['definition'].">".$pageArr[$i];
            }
            $openTags=array();

           
            $offset=0;
            while ($offset<strlen($pageArr[$i])) //memorizzazione in un array dei tag non chiusi
            {
                $ltPos=strpos($pageArr[$i],"<",$offset);
                $gtPos=strpos($pageArr[$i],">",$offset);
               
                if ($gtPos===false || $ltPos===false) break;   

                $tagDefinition=substr($pageArr[$i],$ltPos+1,$gtPos-($ltPos+1)); //tag + attributi

                $spacePos=strpos($tagDefinition," ");
               
                //impostazione nome del tag
                if ($spacePos!==false)
                $tagName=substr($tagDefinition,0,$spacePos);
                else
                $tagName=$tagDefinition;
   
                if ($tagName{0}=="/") //elimina dall'array openTags il tag che si chiude
                {
                    foreach ($openTags as $key=>$val)     //cerca il tag corrispondente
                    if ($val['name']==substr($tagName,1))
                    {
                        unset($openTags[$key]);
                        break;
                    }
                }
                else if (!in_array($tagName,$autoClosedTag)) //tranne i tag che si autochiudiono       
                array_unshift($openTags,array("definition"=>$tagDefinition,"name"=>$tagName));
   
                $offset=$gtPos+1;
            }
   
            foreach($openTags as $val) //chiusura dei tag rimasti aperti
            $pageArr[$i].="</{$val['name']}>";


        }

       
        return $pageArr;
    }
   

Sono evidenziate nei riquadri interni le parti di codice aggiuntive rispetto alla versione base.
La prima riga evidenziata non fa altro che definire un array contenente i nomi dei tag che si autochiudono.

La seconda riga evidenziata aggiunge una condizione nell'IF già esistente nella versione base.
Questo controllo è un raffinamento dell'algoritmo in quanto consente di stabilire se la porzione di testo successiva contiene del testo che non sia costituito solamente da chiusure di tag precedentemente aperti oppure spazi.
In poche parole ciò evita situazioni in cui l'ultima pagina oggetto della suddivizione non contenga alcun testo visibile da parte dell'utente che legge la pagina.
Il controllo esamina il testo rimanente ottenuto con substr($text,$brPos+6) e ci applica la funzione strip_tags() che elimina i tag HTML dal testo e la funzione trim() che riduce eventualmente la stringa contenente solo spazi ad una stringa vuota.
Se il controllo fallisce (la stringa è vuota) allora si aggiunge il rimanente del testo, contenente chiusure di tag, alla pagina attuale senza crearne una nuova ed inutile.

Per quanto riguarda il resto del codice evidenziato, il primo ciclo while riapre i tag manualmente chiusi nella pagina precedente e poi svuota l'array (corrisponde concettualmente ad una estrazione di tutti gli elementi dello stack dalla testa). L'operazione ovviamente viene eventualmente eseguita dalla seconda iterazione del ciclo for principale perchè inizialmente l'array è vuoto.

Nel secondo ciclo while avviene la parte più interessante ovvero la rilevazione dei tag presenti nel testo. Utilizzando una variabile di offset che viene incrementata ad ogni iterazione cerchiamo nel testo i vari tag.
I tag possono ovviamente essere con o senza attributi quindi per rilevare il nome del tag è necessaria qualche operazione aggiuntiva.

Innanzitutto si rileva la posizione dei caretteri < e >. Se i caratteri non sono presenti allora non ci sono più tag e si può uscire dal ciclo while con break.
Successivamente si memorizza nella variabile tagDefinition (definizione del tag)  tutto ciò che è presente tra i due caratteri con questa istruzione:

$tagDefinition=substr($pageArr[$i],$ltPos+1,$gtPos-($ltPos+1));


si ricordi che il terzo parametro della funzione substr è la lunghezza (in numero di caratteri) da considerare dopo il secondo parametro che è l'indice di partenza, e non l'indice di arrivo, quindi è necessario sottrarre alla posizione del carattere di arrivo > (in $gtPos) l'indice di partenza ($ltPos+1).

Per rilevare il nome del tag si cerca all'interno di $tagDefinition lo spazio. Se non c'è uno spazio il tag è senza attributi quindi si memorizza in $tagName lo stesso contenuto di $tagDefinition altrimenti (tag con attributi) si memorizza il testo di $tagDefinition fino alla posizione dello spazio.

A questo punto si esamina il primo carattere di $tagName (si può fare anche con $tagDefinition).

Se è il backslash significa che si tratta della chiusura di un tag e quindi eliminiamo dall'array $openTags (che contiene i tag rilevati aperti) il tag corrispondente semplicemente scorrendolo ed eliminando l'elemento non appena si trova il nome del tag corrispondente.
Come detto precedentemente, questa operazione di estrazione dovrebbe eseguirsi ricercando l'elemento partendo dall'ultimo elemento inserito (testa della pila) fino al primo. In questo caso possiamo ricercarlo scorrendo l'array dalla prima all'ultima posizione perchè l'implementazione della nostra struttura dati è stata fatta inserendo i tag sempre in testa all'array tramite la funzione array_unshift(). Quindi lo scorrimento sequenziale dell'array corrisponde proprio all'operazione voluta.

Se è l'apertura di un tag (che non si autochiude) si aggiunge il tag trovato in testa all'array $openTags tramite la funzione array_unshift. Ogni elemento di $openTags è un array di 2 elementi cioè name e definition che contengono ciò che si memorizza in $tagName e $tagDefinition.

L'ultimo caso riguarda la possibilità di trovare un tag che si autochiude (definiti nell'array $autoClosedTag) che non comporta l'esecuzione di alcuna operazione.

Non rimane che incrementare la variabile $offset su cui si basa la terminazione del ciclo while alla posizione del carattere > ovvero sull'indice di testo dal quale si continueranno a cercare i tag.

Usciti dal ciclo while abbiamo in $openTags i tag rimasti aperti li aggiungiamo alla pagina corrente scorrendo l'array.

A questo punto all'iterazione successiva del ciclo for useremo l'array $openTags per riaprire i tag chiusi manualmente.
Qui entra in gioco la prima parte del codice evidenziato (che viene eseguito solo alle iterazioni successive alla prima).
Questo codice non fa altro che aggiungere prima del testo i tag da aprire usando ovviamente l'elemento definition (tag ed eventuali attributi) di ogni elemento di  $openTags.
L'unica cosa che bisogna notare è che nell'array $openTags, i tag sono presenti dall'ultimo che deve essere aperto al primo in quanto in fase di creazione di questo array, i tag sono aggiunti in testa.
Quindi il ciclo che scorre questo array per riaprirli, deve aggiungere ogni elemento prima dei precedenti ad esempio se nell'array è presente
span,li,ul,div e il testo è CIAO, i passi che deve seguire il ciclo sono


<span>CIAO
<li><span>CIAO
<ul><li><span>CIAO 
<div><ul><li><span>CIAO



Dopo questa operazione l'array viene svuotato e si riparte dall'inizio.

Siamo giunti alla fine, spero che questo articolo vi sia stato utile. Per commenti e qualsiasi altra eventuale domanda non esitate a scrivere una email o ad inviare un commento nel form sottostante.





Votazione articolo: esprimi la tua opinione!

Numero di voti: 3
Voto medio:

Pubblica il tuo commento per questo articolo

codice di verifica

Lista commenti

- Nessun risultato