Markdown in JavaScript nel browser con Marked

Marked è una libreria JavaScript leggera e performante che consente di convertire testo scritto in formato Markdown in codice HTML valido. Nata per essere utilizzata sia in ambiente Node.js sia direttamente nel browser, rappresenta una delle soluzioni più diffuse e consolidate per il parsing del Markdown lato client. In questo articolo analizzeremo nel dettaglio il suo funzionamento attraverso un esempio pratico completo, partendo dalla struttura HTML necessaria fino al codice JavaScript che orchestra la conversione.

Installazione e inclusione nel browser

A differenza dell'ambiente Node.js, dove Marked viene installato tramite npm, nel browser è sufficiente includere lo script della libreria attraverso un tag <script>. La versione più recente è disponibile tramite CDN:

<!-- Inclusione della libreria Marked tramite CDN -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

Una volta caricato lo script, l'oggetto globale marked diventa disponibile nel contesto della pagina. Questo oggetto espone il metodo principale marked.parse(), che accetta una stringa in formato Markdown e restituisce la corrispondente rappresentazione HTML.

Struttura HTML di base

Per costruire un convertitore Markdown funzionante nel browser servono tre elementi fondamentali: un campo di input in cui l'utente scrive il testo Markdown, un pulsante che avvia la conversione e un campo di output in cui viene mostrato il risultato HTML. Una possibile struttura è la seguente:

<!-- Area di testo per l'inserimento del Markdown -->
<textarea id="markdown" rows="10" cols="60"
    placeholder="Scrivi il tuo Markdown qui..."></textarea>

<!-- Pulsante per avviare la conversione -->
<button id="convert" type="button">Converti</button>

<!-- Area di testo per la visualizzazione dell'HTML generato -->
<textarea id="output" rows="10" cols="60" readonly></textarea>

<!-- Inclusione della libreria Marked -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

<!-- Script dell'applicazione -->
<script src="app.js"></script>

Il campo output è contrassegnato come readonly perché il suo contenuto viene generato programmaticamente e non deve essere modificato manualmente dall'utente.

Analisi del codice JavaScript

Il cuore dell'applicazione risiede nel file JavaScript che gestisce la logica di conversione. Esaminiamo il codice nella sua interezza:

'use strict';
(function() {
    // Funzione che converte il contenuto Markdown in HTML
    // utilizzando la libreria Marked
    function markDownToHTML(sourceEl = null, targetEl = null) {
        // Verifica che la libreria Marked sia disponibile
        if(!marked) return;
        // Verifica che gli elementi sorgente e destinazione esistano
        if(!sourceEl || !targetEl) return;
        // Esegue la conversione e assegna il risultato
        // all'elemento di destinazione
        targetEl.value = marked.parse(sourceEl.value);
    }
    // Attende il caricamento completo del DOM
    // prima di registrare gli eventi
    document.addEventListener('DOMContentLoaded', () => {
        // Recupera il riferimento al pulsante di conversione
        const convertBtn = document.getElementById('convert');
        // Se il pulsante non esiste, interrompe l'esecuzione
        if(!convertBtn) return;
        // Recupera i riferimenti agli elementi di input e output
        const sourceEl = document.getElementById('markdown');
        const targetEl = document.getElementById('output');
        // Registra l'evento click sul pulsante
        convertBtn.addEventListener('click', () => {
            markDownToHTML(sourceEl, targetEl);
        }, false);
    }, false);
})();

La direttiva 'use strict'

La prima riga del file attiva la modalità rigorosa (strict mode) di JavaScript. Questa modalità impone regole più stringenti durante l'esecuzione del codice: impedisce l'uso di variabili non dichiarate, vieta l'assegnazione a proprietà di sola lettura e genera errori espliciti in situazioni che normalmente verrebbero ignorate silenziosamente. L'adozione dello strict mode è considerata una buona pratica nello sviluppo JavaScript moderno.

La IIFE (Immediately Invoked Function Expression)

L'intero codice è racchiuso in una IIFE, ovvero una funzione che viene definita e immediatamente eseguita. La sintassi (function() { ... })(); crea un ambito di visibilità (scope) isolato, evitando che variabili e funzioni dichiarate al suo interno inquinino lo scope globale. Questo pattern è particolarmente utile quando si lavora con più script nella stessa pagina, poiché previene conflitti di nomi tra variabili e funzioni definite in file diversi.

La funzione markDownToHTML

La funzione markDownToHTML è il nucleo della logica di conversione. Accetta due parametri, entrambi con valore predefinito null: sourceEl, che rappresenta l'elemento da cui leggere il testo Markdown, e targetEl, l'elemento in cui scrivere il risultato HTML.

La funzione esegue due controlli di sicurezza prima di procedere. Il primo verifica che l'oggetto globale marked sia disponibile: se la libreria non è stata caricata correttamente, la funzione termina senza errori. Il secondo controllo assicura che entrambi gli elementi DOM siano stati forniti come argomenti. Solo dopo aver superato entrambi i controlli, la funzione invoca marked.parse() passando il valore testuale dell'elemento sorgente e assegna il risultato alla proprietà value dell'elemento di destinazione.

La registrazione degli eventi

L'evento DOMContentLoaded garantisce che il codice venga eseguito solo quando il DOM è stato completamente costruito dal browser, indipendentemente dal fatto che risorse esterne come immagini o fogli di stile siano ancora in fase di caricamento. All'interno del gestore di questo evento vengono recuperati i riferimenti ai tre elementi HTML tramite document.getElementById().

Il codice include un ulteriore controllo difensivo: se il pulsante di conversione non viene trovato nel DOM, l'esecuzione si interrompe immediatamente. Questo impedisce errori nel caso in cui il markup HTML non contenga l'elemento atteso. Solo dopo aver ottenuto tutti i riferimenti necessari viene registrato l'evento click sul pulsante, che a sua volta invoca la funzione markDownToHTML.

Il terzo parametro false passato ad addEventListener indica che l'evento deve essere gestito nella fase di bubbling (propagazione dal basso verso l'alto nel DOM) anziché nella fase di capturing (dall'alto verso il basso). Si tratta del comportamento predefinito, ma specificarlo esplicitamente rende il codice più chiaro nelle sue intenzioni.

Il metodo marked.parse()

Il metodo marked.parse() è il punto di ingresso principale della libreria. Accetta una stringa contenente testo Markdown e restituisce una stringa HTML. Supporta la sintassi Markdown standard, inclusi titoli, paragrafi, elenchi puntati e numerati, link, immagini, blocchi di codice, testo in grassetto e corsivo, tabelle e citazioni.

La libreria è inoltre altamente configurabile. È possibile personalizzare il comportamento del parser attraverso il metodo marked.setOptions():

// Configurazione delle opzioni di Marked
marked.setOptions({
    // Abilita le interruzioni di riga con un singolo ritorno a capo
    breaks: true,
    // Attiva il supporto alla specifica GitHub Flavored Markdown
    gfm: true
});

L'opzione breaks modifica il comportamento predefinito del Markdown rispetto agli a capo: di norma un singolo ritorno a capo viene ignorato e per creare un nuovo paragrafo servono due ritorni a capo consecutivi. Attivando questa opzione, ogni ritorno a capo produce un elemento <br>. L'opzione gfm abilita il supporto a GitHub Flavored Markdown, un'estensione della specifica standard che aggiunge funzionalità come le tabelle, i blocchi di codice delimitati da triple backtick, il testo barrato e le liste di attività.

Gestione della sicurezza

Quando si converte Markdown in HTML all'interno del browser, è fondamentale considerare gli aspetti legati alla sicurezza. Se il testo Markdown proviene da fonti non controllate, come l'input di un utente, potrebbe contenere codice JavaScript malevolo incorporato in tag HTML. Per mitigare questo rischio, Marked offre la possibilità di utilizzare una libreria di sanitizzazione come DOMPurify:

// Importazione e utilizzo di DOMPurify per sanificare l'output HTML
// generato dalla conversione Markdown
const rawHTML = marked.parse(sourceEl.value);
// Rimozione di eventuali script o attributi pericolosi dall'HTML
const sanitizedHTML = DOMPurify.sanitize(rawHTML);
targetEl.value = sanitizedHTML;

DOMPurify analizza il codice HTML prodotto da Marked e rimuove qualsiasi elemento o attributo potenzialmente pericoloso, come tag <script>, gestori di eventi inline (onclick, onerror) e URL con schema javascript:. L'uso combinato di Marked e DOMPurify rappresenta una strategia solida per la conversione sicura del Markdown in ambienti in cui l'input dell'utente non può essere considerato attendibile.

Rendering HTML nell'interfaccia

Nel codice di esempio, l'HTML generato viene inserito in un campo <textarea> tramite la proprietà value. Questo approccio mostra il codice sorgente HTML come testo leggibile, utile a scopo didattico o per copiare il markup risultante. Se invece si desidera visualizzare l'HTML renderizzato dal browser, è possibile utilizzare la proprietà innerHTML su un elemento contenitore:

// Funzione che renderizza il Markdown come HTML visibile
// nel browser, anziché mostrarne il codice sorgente
function renderMarkdown(sourceEl = null, targetEl = null) {
    if(!marked) return;
    if(!sourceEl || !targetEl) return;
    // Inserisce l'HTML generato come contenuto dell'elemento,
    // permettendo al browser di renderizzarlo visivamente
    targetEl.innerHTML = marked.parse(sourceEl.value);
}

In questo caso, l'elemento targetEl non sarà più un <textarea> ma un generico contenitore HTML in cui il browser interpreterà e visualizzerà i tag generati da Marked, mostrando titoli formattati, elenchi strutturati, link cliccabili e tutti gli altri elementi del Markdown convertito.

Utilizzo di Marked con i renderer personalizzati

Marked mette a disposizione un sistema di renderer che consente di sovrascrivere il modo in cui specifici elementi Markdown vengono tradotti in HTML. Ad esempio, è possibile fare in modo che tutti i link generati si aprano in una nuova scheda del browser:

// Creazione di un renderer personalizzato per modificare
// il comportamento predefinito dei link
const customRenderer = new marked.Renderer();

// Sovrascrittura del metodo di rendering dei link
// per aggiungere l'attributo target="_blank"
customRenderer.link = function(href, title, text) {
    // Costruzione del tag <a> con apertura in nuova scheda
    const titleAttr = title ? ` title="${title}"` : '';
    return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
};

// Applicazione del renderer personalizzato alla configurazione
marked.setOptions({
    renderer: customRenderer
});

L'attributo rel="noopener noreferrer" è una misura di sicurezza che impedisce alla pagina di destinazione di accedere all'oggetto window.opener della pagina di origine, prevenendo potenziali attacchi di tipo tabnapping.

Demo

JavaScript Marked: Markdown in browser

Conclusioni

La libreria Marked si distingue per la sua semplicità di integrazione e la sua versatilità. Il codice analizzato in questo articolo mostra come, con poche righe di JavaScript, sia possibile costruire un convertitore Markdown funzionante direttamente nel browser. I pattern utilizzati (strict mode, IIFE, controlli difensivi, registrazione degli eventi dopo il caricamento del DOM) rappresentano pratiche consolidate nello sviluppo JavaScript e contribuiscono a rendere il codice robusto e manutenibile.

Per applicazioni più complesse, Marked offre ampie possibilità di personalizzazione attraverso le opzioni di configurazione, i renderer personalizzati e le estensioni. Combinata con strumenti di sanitizzazione come DOMPurify, la libreria può essere impiegata in modo sicuro anche in contesti dove l'input dell'utente rappresenta un potenziale vettore di attacco.