Creando un marco de herramientas con JavaScript

Figura
Figura. Captura de pantalla de la aplicación Web Tools online

Desde hace tiempo vengo usando un marco de herramientas web con PHP en un servidor local (localhost). Explicaba en ese tema que no lo publicaba en el servidor en producción principalmente por los recursos que consume. Quería ir compartiendo todas esas herramientas, con lo que si había alguién interesado tendría que descargarse ese marco de herramientas e instalarlo en su localhost.

Para evitar ese contratiempo he ido replicando herramientas para ejecutar online, como el resaltador de código con JavaScript. Y creando nuevas como el reciente probador de expresiones regulares JavaScript. Pero hay un conjunto de recursos comunes a todas ellas que se podrían compartir en un marco de herramientas basado sólo en JavaScript. Así cuando agregase una nueva herramienta al marco me ahorraría volver a rescribir esos recursos. Bueno, pues ese es el marco de herramientas Web Tools online que presento ahora, cuya captura de pantalla puede ver en la Figura.

La idea de hacer un marco basado en JavaScript no es nueva. La gran ventaja es que podría ejecutarse online sin necesidad de instalar nada. De hecho ya me lo planteé cuando cree el que tengo basado en PHP. El principal problema era que un marco de herramientas necesitaba obligatoriamente acceder al sistema local de archivos. Tareas como resaltar el código fuente de un archivo html necesitan leer ese archivo para extraer el contenido de texto. Y en otros casos poder escribirlos también cuando la herramienta precise modificarlos. PHP tiene un completo soporte de gestión del sistema local de archivos. Pero no así JavaScript por motivos de seguridad. Por ahora y de forma general con JavaScript sólo podemos leer archivos. Veámos como están las cosas.

Hay un FileReader para manejar la lectura de archivos sin ninguna limitación de acceso. No hay problemas con esto porque se usa para enviar archivos al servidor, funcionando conjuntamente con un <input type="file"> o bien usando Drag and Drop para arrastrar y soltar archivos. No hay forma de que un script abra y lea por sí mismo un archivo sin que el usuario lo haya seleccionado manualmente.

Por otro lado hay referencias de trabajos en desarrollo para implementar FileSystem y FileWriter, pero están parcialmente soportados sólo por Chrome. Para preservar cierto grado de seguridad la gestión se restringue a un espacio sandboxed. Es decir, no podemos gestionar cualquier archivo o carpeta del sistema, sólo un espacio que el navegador destinaría para su uso ubicado en las carpetas del usuario del sistema operativo (Mis Documentos, por ejemplo).

Esa limitación del sandboxed, aparte de que no hay soporte general para otros navegadores (solo Chrome por ahora), hace que descarte esa posibilidad. Sólo nos queda el FileReader con un raquítico <input type="file">, por lo que cabe preguntarse hasta dónde podemos llegar con sólo estos recursos.

Usando el input type file con multiple, directory y relativePath

Supongamos que tengo todo el material en desarrollo de mi sitio web en mi ordenador en una carpeta denominada "wx" ubicada en cualquier sitio. Esa carpeta se correspondería con la carpeta raíz del sitio en desarrollo y, posteriormente, todo su contenido iría a la carpeta equivalente del sitio en producción. Con los navegadores puedo seleccionar uno o más archivos de una misma carpeta con un <input type="file"> y luego leerlos con el FileReader. El INPUT TYPE FILE nos suministrará los nombres de archivos pero no la carpeta donde están ubicados. Todos los navegadores se comportan así, pero Chrome además ofrece la propiedad relativePath (ruta relativa), que junto a un atributo directory permite la selección de todos los archivos incluidos recursivamente dentro de una carpeta (cualquier carpeta) que seleccione el usuario:

Figura
Figura. INPUT TYPE FILE para seleccionar carpetas en Chrome + Windows

En la Figura puede ver el cuadro de diálogo del sistema operativo que se abre en Chrome para seleccionar una carpeta. Aunque dice Seleccionar una carpeta para subirla realmente no vamos a subir nada al servidor. Solo queremos cargar la propiedad files del INPUT TYPE FILE con todos los archivos que cuelgan de la carpeta seleccionada y de todas sus carpetas descendientes. Las propiedades del INPUT nos dan esa información:

Figura
Figura. INPUT TYPE FILE y sus propiedades files, multiple y directory.

En la Figura podemos ver la propiedad files, donde vamos a tener un FileList, una lista de archivos. En esa composición de imagen puede ver el archivo de la primera posición de la lista, con sus propiedades como nombre, tamaño, fecha de última modificación y tipo de archivo. En este caso es un .htaccess y el navegador no lo reconoce, pero si fuese un HTML aparecería el Mime Type text/html. Por último vemos la propiedad del archivo webkitRelativePath, que es el relativePath con el prefijo webkit. Nos suministra la ruta relativa a la carpeta seleccionada. Y esto si que es importante, porque tendremos en el FileList las rutas relativas de todos los documentos de nuestro sitio en desarrollo. Vemos también los atributos multiple para selección de varios archivos y webkitdirectory para selección por carpetas. Con todo esto podemos construirnos un explorador de carpetas y archivos a medida para nuestro Web Tools online:

Figura
Figura. Apariencia de un explorador de carpetas y archivos para Web Tools online

Como se ve en la figura Figura, una vez cargado el FileList y mediante JavaScript podemos explorar carpetas, bajando y subiendo por el árbol de carpetas hasta la carpeta raíz (la seleccionada por el usuario); seleccionar archivos y carpetas; abrir archivos para ver su contenido; filtrar una lista de rutas para realizar un determinado proceso; en definitiva, realizar acciones con todos los recursos de nuestro sitio web en desarrollo. Y todo eso sólo con JavaScript, sin salir de la misma página abierta en el navegador.

Dígamos mejor acciones de lectura, porque aún no podemos modificar el sistema local de archivos. Una tarea requerida podría ser leer un archivo de texto, volcarlo en un <textarea>, modificar el texto y luego sobrescribir el archivo. O simplemente guardar contenidos de texto resultantes de la aplicación en nuevos archivos ¿Qué podemos hacer para guardar archivos locales sin que entre en juego el servidor web?

Guardando archivos locales

No podemos guardar directamente un archivo pero si podemos descargarlo. O simular que lo vamos a descargar, porque el contenido no vendrá del servidor.

Figura
Figura. Un botón con el título "Descargar" nos abre el Bloc de Notas de Windows con el contenido de texto del archivo que queremos guardar.

Como se observa en la Figura, la descarga se hace en un archivo de texto, de tal forma que se abre el Bloc de Notas de Windows con el contenido. A partir de ese momento ya podemos guardar ese archivo como nuevo o bien sobreescribir otro existente. Para que se abra el Bloc de Notas o aplicación similar en el Sistema Operativo es necesario agregar extensión "txt" al nombre del archivo que se descarga. En otro caso se produce la descarga pero no tiene porque abrirse el archivo.

El script para realizar la falsa descarga es el siguiente:

/* Descarga un contenido de texto con un nombre de archivo, abriéndose
el panel de descarga en el navegador. Si pasamos extensión ".txt" el 
navegador Chrome abrirá automáticamente el Bloc de Notas de Windows 
con el contenido. Script adaptado de varias cosas vistas en
http://stackoverflow.com/questions/2897619/using-html5-javascript-to-generate-and-save-a-file
Probado satisfactoriamente en CH39, FF33, OP26, IE11, IE10
En IE9 e IE8 se queda como cargando página sin dar error pero no 
termina de descargar nada.
*/
descargarArchivo: function(nombreArchivo, contenido) {
    var hRef, crearVinculo = true;
    if (typeof Blob != "undefined") {
        var blob = new Blob([contenido], {type: "application/octet-stream"});
        if (typeof window.navigator.msSaveBlob != "undefined"){
            //IE11,IE10 salen por aquí y no es necesario hacer nada más.
            window.navigator.msSaveBlob(blob, nombreArchivo);
            crearVinculo= false;
        } else {
            //CH39,FF33 y OP26 salen por aquí
            hRef = window.URL.createObjectURL(blob);
        }
    } else {
        //IE9,IE8 salen por aquí, pero lo siguiente ya no funciona. Hasta ahora 
        //no he encontrado algo para estos navegadores.
        hRef = "data:application/octet-stream," + encodeURIComponent(contenido);
    }
    if (crearVinculo){
        var vinculo = document.createElement("a");
        vinculo.href = hRef;
        vinculo.download = nombreArchivo;
        vinculo.style.display = "none";
        document.body.appendChild(vinculo);
        vinculo.click();
        document.body.removeChild(vinculo);
    }
},

Se trata de crear un Blob que representa un tipo de datos de archivo. Puedes ver más en HTML5 File API o en la especificación W3C: The Blob Interface and Binary Data. Luego usamos el método createObjectURL(blob) de window.URL para crear una cadena de texto para insertar en un atributo href de un <a>. Creamos un elemento vínculo temporal y lo agregamos al DOM, ejecutándolo con vinculo.click(), tras lo cual se descargará el archivo y luego podemos eliminar ese elemento. El contenido no viene, por tanto, descargado del servidor, todo se desarrolla en el navegador.

Recuperando y guardando la configuración de herramientas

Otro aspecto imprescindible para un marco de herramientas web es poder guardar la configuración así como recuperarla en momento posterior. De esta forma personalizamos el marco a nuestras necesidades. Para ello inicialmente debemos crear un archivo de texto vacío con el nombre wt.txt y ubicarlo en cualquier carpeta que luego incorporaremos al cargar el Sistema de Archivos Locales.

Figura
Figura. Cargar varias carpetas al Sistema de Archivos Locales. En alguna de ellas puede haber un archivo wt.txt con la configuración.

Como se observa en la Figura, podemos cargar todas las carpetas que deseemos. En cada carga se buscará un primer archivo con nombre "wt.txt", a partir de lo cual la aplicación entra en modo Local. A partir de aquí si se cargan nuevas carpetas se ignoran otros archivos con igual nombre "wt.txt". Los cambios en los campos de configuración podrán ser salvados posteriormente descargando un archivo de texto para sobrescribir ese "wt.txt" con el botón "Exportar" de la pestaña Configuración del Marco principal.


En resumen, un nuevo Marco de Herramientas Web para probar, por ahora sólo con algunas herramientas y con la esperanza de seguir incorporando otras a este marco.