Dizque



CSS, Desarrollo web, JavaScript, Programando

LIPT: Inyección de listas a gogó

Saltar a Anotaciones relacionadas

Hoy por hoy (19 de junio de 2007) ya no uso LIPT en este sitio, así que no podrás ver (al menos aquí) el resultado de la aplicación del script. Pero sí puedes visitar Sentido Web, donde sigue en uso, o la demo que preparó Ingo Chao cuando parimos este asunto.

¿Cómo puedo hacer para imprimir con cierta dignidad mis listados de código? Actualmente utilizo etiquetas <pre>. Al visualizar el código en pantalla no hay mayor problema, ya que puedo utilizar barras de desplazamiento, pero al imprimir, cielo santo, si una línea ocupa un ancho mayor que la superficie imprimible, queda truncada.

Más o menos esto —versión libre— es lo que preguntaba Matthieu Petiot al comienzo del hilo a problem with the impression of the tag pre en CSS-Discuss. Te sugiero una lectura del mismo como introducción a lo que aquí se va a decir. Ya, vale, prefieres que te haga un resumen.

En la primera respuesta, Philippe Wittenbergh propone diversas soluciones utilizando únicamente CSS. A continuación entra en acción Ingo Chao que sugiere utilizar una técnica de inyección vía javascript (alla TILT) y promete una medalla de chocolate para el primero en desarrollarla.

LIPT
List injection for pre tags
Inyección de listas en etiquetas pre

Aquí es donde servidor se pone manos a la obra. Una vez desarrollado y aplicado el script, el código queda tal que así (evidentemente, solo podrás apreciar sus efectos si navegas con javascript activado):

function foo() { // Just a demo
    //doSomething();
    doNothing();
}

Pero, ¿qué es lo que hace?

Pues, para empezar, no incordiar en absoluto. Siguiendo los principios de DHTML no intrusivo —o Unobstrusive Javascript si lo prefieres—, este script no requiere la adición de marcado superfluo en el documento objetivo. Lo único que tendrás que hacer para utilizarlo es llamarlo desde algún lugar de La Mancha, digo, desde algún lugar del head del documento, esto es:

<script src="/path/to/javascript/files/lipt.js" type="text/javascript"></script>

Y ya está. Cuando se complete la carga del documento, el script entrará en acción. Esto es lo que hace:

  1. Localiza el primer elemento code dentro de cada uno de los elementos pre presentes en el documento.

  2. Extrae el texto que contiene.

  3. Divide el contenido en líneas y lo carga en un array.

  4. Genera una lista ordenada (ol) con clase «code».

  5. Para cada línea, se crea un ítem (li), se le asigna una clase (class) conforme a la indentación de dicha línea en el código (tab0, tab1, etc.) y dentro del ítem se coloca la línea dentro de un elemento code.

  6. Se sustituye el pre por nuestra ordenadísima lista.

Este es el marcado presente en el documento antes de aplicar el script:

<pre>
<code>
function foo() { // Just a demo
    //doSomething();
    doNothing();
}
</code>
</pre>

Después de aplicarlo, queda esto:

<ol class="code">
    <li class="tab0"><code>function foo() { <span class="cmt">// Just a demo</span>&nbsp;</code></li>
    <li class="tab1"><code><span class="cmt">//doSomething();</span>&nbsp;</code></li>
    <li class="tab1"><code>doNothing();&nbsp;</code></li>
    <li class="tab0"><code>}&nbsp;</code></li>
</ol>

Como se puede apreciar, también se marcan los comentarios como tales. Atento, criatura, aparecen dentro de etiquetas span con clase «cmt».

Ahora todo lo que falta por hacer es aplicar el CSS que mejor nos parezca para presentar el listado a nuestro gusto. He aquí el que utilizo (a día de hoy) en Dizque:

ol.code {
    border: 1px solid #e0f0e0;
    background: #CBDABB;
    margin-left: 0;
    list-style-type: decimal-leading-zero; /* IE6 ignores this :( */
    padding: .2em .2em .2em 3em;
    font-family: courier, monospace;
}

ol.code li {
    margin-bottom: 1px;
    line-height: 1.5;
    background: #D8E7C8;
}

ol.code li .cmt {
    color: #060;
}

li.tab0 { padding-left: .4em; }
li.tab1 { padding-left: 2.4em; }
li.tab2 { padding-left: 4.4em; }
li.tab3 { padding-left: 6.4em; }
li.tab4 { padding-left: 8.4em; }
li.tab5 { padding-left: 10.4em; }
li.tab6 { padding-left: 12.4em; }
li.tab7 { padding-left: 14.4em; }
li.tab8 { padding-left: 16.4em; }

Precedentes

Haberlos, haylos. El marcado resultante es bastante parecido —o muy similar, según se mire— al propuesto por Simon Willinson en uno de sus experimentos y adoptado por Dunstan Orchard, que lo genera en el servidor mediante PHP. Los estilos definidos en CSS también han salido a sus padres, fíjate tú.

La opinión del autor

Debo confesar que, a pesar de que estoy utilizando el script aquí mismito, me parece una solución poco apropiada. Si realmente queremos obtener este resultado, deberíamos realizar el proceso en el servidor. (En PHP, puedes utilizar, por ejemplo, el paquete de PEAR Text_Highlighter, que hace muchas más virguerías que el script aquí presentado.)

Ahora bien, será poco apropiada pero me ha quedado de puta madre: trabaja correctamente en todos los navegadores modernos, (incluso en Internet Explorer) y en los navegadores de l’any de la picor o sin soporte para javascript deja el código tal cual (aconsejo crear algunas reglas CSS para estos casos, como se hace en este sitio). Ah, y funciona ya sirvas el documento como text/html o como application/xhtml+xml.

El script

/*
 * Lipt: List injection for PRE tags
 * Author: Choan C. Galvez <choan(a)alice.0z0ne.com> <http://dizque.lacalabaza.net>
 * Suggested by Ingo Chao <http://www.satzansatz.de/cssd/listinjection.html>
 * Version: 0.2.3
 *
 * License: Whatever you like. Provided as is, no warranty, blah blah. Use at your own risk.
 *
 * Changelog:
 * 2005-07-02 Anonymous function
 * 2005-07-02 Just one '//' comment by line
 * 2005-07-01 Checks the HTML default namespace and creates the elements using it
 */
(function() { // anonymous function, doesn't pollute the global namespace
function lipt() {

    /* change to FALSE if you don't want to proccess comments (boogie buggy) */
    var PROCCESS_COMMENTS = true; 

    if (!document.getElementsByTagName) {
        return;
    }

    var ns = document.getElementsByTagName('html')[0].getAttribute('xmlns');

    // look for pre tags in the doc
    var pres = document.getElementsByTagName('pre');

    if (0 == pres.length) {
        return; // no pre tags, nothing to do
    }

    for (var i = 0; i < pres.length; i++) {
        var pre = pres[i];

        // search for the first code element inside the pre
        var code = pre.getElementsByTagName('code')[0];

        if (null == code) {
            continue; // no one here, try with the next pre tag
        }

        // go for the job
        var inMultiLineComment = false;
        var inHtmlComment = false;

        var content = getText(code);

        // normalize new lines
        if (!window.opera) { /* Opera seems to have a nice bug with global replacements */
            content = content.replace(/\n|\r|\r\n/g, '\n');
        } else {
            content = content.replace(/\n|\r|\r\n/, '\n');
        }
        content = content.replace(/^\n*/, ''); /* trim empty lines at start */
        content = content.replace(/\n*$/, ''); /* trim empty lines at the end */

        var lines = content.split('\n');

        var ol = createElement('ol', ns);
        ol.className = 'code';

        for (var j = 0; j < lines.length; j++) {
            var line = lines[j];
            line = line.replace(/\t/g, '    '); // replace tab with four spaces

            var cname = 'tab' + (Math.floor(countSpaces(line) / 4)); // className for this line
            var restSpaces = countSpaces(line) % 4;
            line = line.replace(/^ +/, '');
            if (restSpaces) {
                for (var k = 0; k < restSpaces; k++) {
                    line = '\u00A0' + line; /* &nbsp; equivalent in Unicode */
                }
            }
            var parts = new Array();

            if (inMultiLineComment || inHtmlComment) {
                parts = ['', line];
            } else {
                parts = [line];
            }

            if (PROCCESS_COMMENTS) {
                var slashSlashPos = line.indexOf('//');
                var starSlashPos = line.indexOf('/*');
                var slashStarPos = line.indexOf('*/');
                var htmlCmtStart = line.indexOf('<!--');
                var htmlCmtEnd   = line.indexOf('-->');

                labelSlashSlash: if (slashSlashPos != -1) {
                    switch (line.charAt(slashSlashPos -1)) {
                        case '"':
                        case "'":
                        case ':': /* don't process URIs as comments */
                            break labelSlashSlash;
                    }
                    //parts = line.split('//');
                    //parts[1] = '//' + parts[1];
                    parts[0] = line.substring(0, slashSlashPos);
                    parts[1] = line.substring(slashSlashPos)
                } else if (starSlashPos != -1) {
                    switch (line.charAt(starSlashPos -1)) {
                        case '"':
                        case "'":
                            break labelSlashSlash;
                    }
                    if (!inMultiLineComment) {
                        parts = line.split('/*');
                        parts[1] = '/*' + parts[1];
                    }
                    inMultiLineComment = true;
                }

                labelHtmlCmt: if (htmlCmtStart != -1) {
                    switch (line.charAt(htmlCmtStart -1)) {
                        case '"':
                        case "'":
                            break labelHtmlCmt;
                    }
                    if (!inHtmlComment) {
                        parts = line.split('<!--');
                        parts[1] = '<!--' + parts[1];
                    }
                    inHtmlComment = true;
                }


                if (slashStarPos != -1) {
                    inMultiLineComment = false;
                }

                if (htmlCmtEnd != -1) {
                    inHtmlComment = false;
                }
            }

            var li = createElement('li', ns);
            li.className = cname; // tab0, tab1, etc.
            var span = createElement('code', ns);
            var code = document.createTextNode(parts[0]); 
            span.appendChild(code);


            if (parts[1]) {
                var cmt = createElement('span', ns);
                cmt.appendChild(document.createTextNode(parts[1]));
                cmt.className = 'cmt';
                span.appendChild(cmt);
            }

            span.appendChild(document.createTextNode('\u00A0')); /* Unicode non-breaking space fix for Firefox */

            li.appendChild(span);
            ol.appendChild(li);
        };

        pre.parentNode.replaceChild(ol, pre);
        i--; // IMPORTANT: if we replace the pre, now there are n - 1 pre elements in the collection we are traversing
    };

    function countSpaces(s) {
        var spaceCount = 0;
        for (var i = 0; i < s.length; i++) {
            if (' ' == s.substr(i,1)) {
                spaceCount++;
            } else {
                break;
            }
        };
        return spaceCount;
    }

    function getText(node) {
        /* from JavaScript The Definitive Guide */
        var s = '';
        var children = node.childNodes;
        for (var i=0; i < children.length; i++) {
            var child = children[i];
            if (child.nodeType == 3 /* Text node */) {
                s += child.data;
            } else {
                s += getText(child);
            }
        }
        return s;
    }

    function createElement(element, ns) {
        // Adapted from Simon Willinson's code 
        // <http://simon.incutio.com/archive/2003/06/15/javascriptWithXML>
        if (ns && typeof document.createElementNS != 'undefined') {
            return document.createElementNS(ns, element);
        }
        if (typeof document.createElement != 'undefined') {
            return document.createElement(element);
        }
        return false;
    }
}

// add the handler to the onload event
var oldOnload = window.onload;
if (typeof oldOnload == 'function') {
    window.onload = function() {
        oldOnload();
        lipt();
    }
} else {
    window.onload = lipt;
}
})(); // execute

Puedes descargarlo y utilizarlo como te plazca, modificarlo, mejorarlo, empeorarlo, cantarle una canción… pero siempre con cariño.




2 comentarios RSS

Pues si no lo recomiendas, no lo usare, pero como tu dices, te ha quedado de puta madre. Palmadita en la espalda pa ti ;)

  • #2
  • choan
  • 2006-03-20 09:36:31

Bill, no es que no lo recomiende, es que considero que es más adecuado realizar el proceso en el servidor.

Ahora bien, cuando no podemos trastear con lo que escupe el servidor, LIPT es la polla (como el Pollo Popeye).


Di la tuya

Puedes usar markdown y estas etiquetas HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong> . Por favor, evita el abuso de las mayúsculas y cuida la ortografía.