Per lo sviluppo di un progetto che sto portando avanti, mi sono trovato nella necessità di gestire e manipolare delle stringhe di testo raggruppandone i contenuti all’interno di array.

Definita una stringa di testo, l’esigenza è quella di avere tre differenti array che contengano rispettivamente lettere, parole e le linee che compongono il paragrafo.

La generazione degli array di lettere e parole è piuttosto semplice perché si può ottenere facilmente con il metodo split() di Javascript:

const string = "La mia stringa di testo";

var lettere = new Array();
var parole = new Array();

lettere = stringa.split("");
//["L", "a", "m", "i", …, "s", "t", "o"]
parole = stringa.split(" ");
//["La", "mia", "stringa", "di", "testo"]

Il caso in esame

Ipotizziamo invece il caso di una stringa di testo sufficientemente lunga con una struttura html di questo tipo:


<div class="container">
<p class="myText">Alëksej Fedorovič Karamazov era
il terzo figlio di un proprietario terriero del nostro
distretto, Fëdor Pavlovič Karamazov, assai noto ai
suoi tempi  (e del resto ancor oggi ricordato fra noi) 
per la sua tragica e oscura fine, avvenuta esattamente 
tredici anni fa e della quale parlerò a tempo debito.</p>
</div>

e associati i seguenti stili:

.container {
   width: 100%;
 }

.myText {
   font-family: 'Roboto Mono', monospace;
   font-size: 16px;
   font-weight: 300;  
}

L’obiettivo sarebbe quello di ottenere un array che aggregasse ogni linea del paragrafo individuando le line-break. Ad esempio:

var linee = new Array();

//["Aleksej Fëdorovič Karamazov era il terzo",
// "figlio di un proprietario terriero del nostro",
// "...",
// "esattamente tredici anni fa e della quale",
// "parlerò a tempo debito."]

Il problema

Ma in questo caso, avendo un layout fluido, la stringa di testo si adatta automaticamente al suo contenitore. È il browser stesso che gestisce il word-wrap. Quindi, non avendo un parametro associato all’interruzione di linea, il metodo split() non potrà essere utilizzato. per generare rapidamente il nostro array di linee.

In effetti la questione si è rivelata subito più complessa del previsto. Ho effettuato un po’ di ricerche ma fra le varie soluzioni ho trovato solo dei metodi relativi al recupero dei caratteri speciali attraverso la classe RegExp e il metodo match(). Purtroppo il sistema non funziona nel nostro caso, poiché, trattandosi di un layout fluido non sono stati definiti dei punti di interruzione di linea.

Esistono anche degli script che hanno già affrontato e risolto la questione, vedi lo SplitText della famiglia dei prodotti GSAP. Si tratta di un prodotto a pagamento e comunque sovradimensionato rispetto alle mie esigenze.

In assenza di una soluzione valida, ho dovuto adattarmi a escogitare un mio sistema “artigianale”. Con l’occasione, ho potuto anche approfondire lo studio di Javascript. Per quest’ultimo motivo ho escluso categoricamente il ricorso a JQUery.

La soluzione

L’idea di fondo è quella di recuperare la larghezza massima del blocco di testo e, attraverso un loop, inserire progressivamente in un array gli elementi della stringa misurando la lunghezza della riga.
Confrontando all’interno del loop larghezza e lunghezza sarà possibile inserire un carattere di interruzione di riga.

Bisogna, però, tenere presente che l’interruzione di linea deve mantenere integra la parola. Quindi il carattere di interruzione di linea deve essere inserito successivamente all’ultima parola intera inseribile all’interno della riga senza eccedere la lunghezza massima.

Per questo esercizio utilizzo un carattere monospace, che semplifica un po’ le cose. Nel caso di un font con kerning occorre modificare qualche punto, ma la logica generale è molto simile. Di seguito l’elenco di tutti i passaggi:

  1. Recuperare la larghezza del box di testo.
  2. Generare un array a due dimensioni contenente ogni parola divisa per singolo carattere, spazi vuoti inclusi.
  3. Misurare la larghezza del singolo carattere. (Nel caso dei caratteri monospace rimane costante al variare del carattere).
  4. Aggiornare il contenuto box di testo, inserendo progressivamente ogni lettera immagazzinata nell’array e misurare la larghezza della linea.
  5. Confrontare la larghezza della linea con la larghezza complessiva del testo e inserire il carattere di interruzione linea.
  6. Generare un nuovo array con il metodo split() assegnando come parametro il carattere di interruzione linea.

1. 2. 3. Larghezza del box e creazione dell’array

//Recupero dei dati e generazione delle variabili
 var item = document.querySelector(".myText");
 var myWidth = item.clientWidth;
 var myString = item.innerHTML;

//Popolamento dell'array:
 var myChars = myString .split("");
 for (let i=0; i<myChars.length; i++) {
   myChars[i] += " ";
   myChars[i] = myChars[i].split("");   
 }

4. Larghezza del singolo carattere

Per risolvere questo passaggio mi sono servito di una soluzione discussa su Stack Overflow e spiegata anche qui. Sostanzialmente si sfrutta il metodo measureText() dell’elemento HTML canvas. Ecco il codice:

function getTextWidth(text, font) {
  var canvas = getTextWidth.canvas || 
               (getTextWidth.canvas = 
               document.createElement("canvas"));
  var context = canvas.getContext("2d");
  context.font = font;
  var metrics = context.measureText(text);
  return metrics.width;
  }
  getTextWidth("Measure me!", 
               "300 16px 'Roboto Mono', monospace");

…con un problema di interpretazione dei browser da risolvere

Recuperiamo le caratteristiche del font:

let w = getComputedStyle(item).fontWeight;
let s = getComputedStyle(item).fontSize;
let f = getComputedStyle(item).fontFamily;
var font = w.concat(" ",s," ",f);  

Utilizziamo il nostro metodo di misura applicato alla lettera “a”. Ricordo che si tratta di un font monospace, quindi è indifferente il carattere scelto:


getTextWidth("a", font);

Applicando il metodo su differenti browser ci accorgiamo immediatamente di un problema: il valore della larghezza riportato cambia utilizzando Chrome oppure Firefox:


//font-size: 16px
var letterWidth = getTextWidth("a", font);
//Chrome output: 8.796875
//Edge output: 8.796875
//Firefox output: 9.60000

Dopo una serie di verifiche, confrontando il risultato con dimensioni differenti, è emerso che la differenza di valore è di circa 1/20 delle dimensioni del font. Possiamo quindi applicare questo fattore correttivo per avere un risultato omogeneo tra i browser:


var letterWidth = getTextWidth("a", font);

if(browser == "Chrome"){
//recupero la dimensione del font
let s = parseInt(getComputedStyle(item)
        .fontSize.replace(/px/g, ''));
//calcolo il fattore correttivo
let f = s/20;
//aggiorno il valore della larghezza
letterWidth += f;
}
 

5. 6. 7. Il loop di confronto

Siamo arrivati all’ultimo passaggio. Ora che abbiamo tutti gli elementi, possiamo utilizzare un ciclo for per navigare all’interno del nostro array di lettere e inserire il carattere di interruzione di linea:

let measure = 0;

 for(let i=0; i<myChars.length; i++) {
         for(let j=0; j<myChars[i].length; j++) {

         measure += letterWidth;
         if (measure > myWidth){
            measure = 0;
            myChars.splice(i, 0, '\n');
       }
    }
}

A questo punto possiamo generare l’array di linee:

let arr = [];
  for(let i=0; i<myChars.length; i++) {
          for(let j=0; j<myChars[i].length; j++) {
              arr.push(myChars[i][j]);
          }
       }
let t = arr.join('');  
let splitLines = t.split('\n');

//conta il numero di righe del paragrafo 
console.log(splitLines.lenght);

Di seguito una demo:

See the Pen Multiline String Split by Matteo (@matteopi) on CodePen.

Come si può vedere, il codice funziona abbastanza bene, anche se il sistema di misura della lunghezza della riga non è esente da errori legati principalmente al comportamento del browser. Certamente, se troverò altre strade per ottenere in maniera più efficiente un risultato più preciso, sarò felice di percorrerle.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.