Cita del Día

Sobre estas líneas se debería ver (¿funciona?) mi último proyecto: la Cita del Día para Blogger. De hecho, debería poder instalarse en cualquier sitio. Sólo necesita que JQuery esté disponible en la página, aunque sería bastante fácil reescribir el código para eliminar la dependencia. Las citas están publicadas en una hoja de cálculo de Google Spreadsheets.

[¡Peligro! ¡Palabrería informática!] El protocolo implementado por las APIs de GData, como extensión de Atom, permite manipular documentos de todo pelaje alojados en los múltiples servicios de Google. Una pequeña adición, la posibilidad de disponer de los resultados en formato JSON, y más concretamente, como JSON evaluado en un script, permite superar gran parte de las limitaciones de seguridad impuestas por los navegadores sobre Ajax. Es posible así realizar cross-site scripting, incorporando contenidos de documentos disponibles mediante feeds de GData en páginas web, sin disponer de control sobre el servidor.

Necesitamos una hoja de cálculo publicada con dos columnas (citas y autores).

http://spreadsheets.google.com/pub?key=pK3d1irR7cVxUSt9KBb8GFw&output=html&gid=0&single=true&range=A1:B4

Un pequeño script permitirá extraer el contenido de esta hoja conociendo algunos de sus datos:

  • Su clave de publicación (key).
  • El identificador único de la página a publicar (una hoja puede tener varias páginas).
  • El rango de celdas del que se va a extraer la información.

Esos datos pueden obtenerse a partir de la URL de publicación de feeds que aparece en las “opciones adicionales de publicación” de la pestaña “Publicación” de una hoja de cálculo. Armados con la información apropiada, podemos programar algo interesante:

var CiteSpreadsheet = {
    'rows': 159,
    'getJsonUrl': function(idx) {
        var baseSpreadsheet = 'http://spreadsheets.google.com/feeds/';
        var feedType = 'cells/';
        var key = 'o06661241339204957995.1659223542184495700/';
        var sheetId = 'od6/';
        var feedAccess = 'public/basic?';
        var range = 'range=A' + idx + ":B" + idx;
        var output = '&alt=json-in-script';
        var callback = '&callback=CiteSpreadsheet.processJsonResponse';
        return baseSpreadsheet + feedType + key + sheetId + feedAccess + range + output + callback;
    },
    'createScriptElem': function(url) {
        var html = '<script type="text/javascript" src="' + url + '"></script>';
        $(html).appendTo('body');
    },
    'getIndex': function() {
        var today = new Date();
        var startDate = new Date(today.getYear(), 0, 1);
        var delta = today.getTime() - startDate.getTime();
        var dayOfYear = Math.ceil(delta / 86400000);
        return dayOfYear % CiteSpreadsheet.rows;
    },
    'processJsonResponse': function(jsonData) {
        var retrievedContent = "<span>" + jsonData.feed.entry[0].content.$t +
            " <cite>" + jsonData.feed.entry[1].content.$t + "</cite></span>";
        $(".cite").append(retrievedContent);
    }
};

CiteSpreadsheet.createScriptElem(
    CiteSpreadsheet.getJsonUrl(
        CiteSpreadsheet.getIndex()));

El objeto del código es insertar, dinámicamente, una marca <script> en el documento actual, que apunte mediante su atributo src a una URL específica de consulta válida para la hoja de cálculo deseada, con formato de salida json-in-script, lo que devuelve un script listo para ejecutar con los datos pedidos en un objeto (en el enlace anterior se describe su estructura; Firefox, Firebug y un punto de ruptura ayudan considerablemente).

El feed obtenido ofrecerá información sobre celdas concretas de la hoja en el rango An:Bn, donde n es el número de fila deseado. Como en la hoja hay 159 citas, para dar una diferente cada día calculamos el índice como el número del día actual dentro del año módulo el total de filas.

Por último, para incorporar la cita al DOM de la página, necesitamos un elemento al que anclarlo. Para ello, se incluye en la plantilla de la bitácora, en el lugar donde deba aparecer la cita, un módulo HTML/Javascript con el contenido

<span class="cite"/>

(podría valer un <div>). En otras plataformas de blogging esto se podrá hacer editando directamente la plantilla o de algún otro modo.

Anuncios

Quote of the Day

You should see over these lines the result of my last project: a Quote of the Day implementation for Blogger It should be a piece of cake to port to other blogging platforms: the only requirement for the code to work is to have a recent version of the JQuery Javascript library loaded. Rewriting the code to eliminate that dependency should also be easy. Quips are published on Google Spreadsheets.

[Warning! Buzzword-enabled text ahead!] GData APIs implement an Atom extension that enables remote document querying and editing. What’s more, query results can be obtained as evaluated JSON objects; this technique allows to bypass most cross-site scripting security constraints, so Ajax widgets can be implemented without control over the server side.

We will need a published spreadsheet sporting quips and authors in two columns.

http://spreadsheets.google.com/pub?key=pK3d1irR7cVxUSt9KBb8GFw&output=html&gid=0&single=true&range=A1:B4

A tiny little script will get us the sheet’s contents. We only have to look for these pieces of information in order to make it work:

  • Publishing key.
  • Unique identifier for the desired sheet (a spreadsheet is organized as a book, so it can contain several sheets).
  • Desired cell range.

These can be obtained from the feed publishing URL that appears in Publish/More publishing options in the Google Spreadsheets interface. There goes the script:

var CiteSpreadsheet = {
    'rows': 159,
    'getJsonUrl': function(idx) {
        var baseSpreadsheet = 'http://spreadsheets.google.com/feeds/';
        var feedType = 'cells/';
        var key = 'o06661241339204957995.1659223542184495700/';
        var sheetId = 'od6/';
        var feedAccess = 'public/basic?';
        var range = 'range=A' + idx + ":B" + idx;
        var output = '&alt=json-in-script';
        var callback = '&callback=CiteSpreadsheet.processJsonResponse';
        return baseSpreadsheet + feedType + key + sheetId + feedAccess + range + output + callback;
    },
    'createScriptElem': function(url) {
        var html = '<script type="text/javascript" src="' + url + '"></script>';
        $(html).appendTo('body');
    },
    'getIndex': function() {
        var today = new Date();
        var startDate = new Date(today.getYear(), 0, 1);
        var delta = today.getTime() - startDate.getTime();
        var dayOfYear = Math.ceil(delta / 86400000);
        return dayOfYear % CiteSpreadsheet.rows;
    },
    'processJsonResponse': function(jsonData) {
        var retrievedContent = "<span>" + jsonData.feed.entry[0].content.$t +
            " <cite>" + jsonData.feed.entry[1].content.$t + "</cite></span>";
        $(".cite").append(retrievedContent);
    }
};

CiteSpreadsheet.createScriptElem(
    CiteSpreadsheet.getJsonUrl(
        CiteSpreadsheet.getIndex()));

What the code does, in short, is dynamically inserting a <script> tag in the document, with its src attribute pointing to a specific query URL for the spreadsheet. Output format should be json-in-script; this returns a ready-to-execute script that wraps data in a Javascript object. Its general format is described in the previous link, but Firefox, Firebug and a judicious break point go all the way to get a hold on the gory details.

We’ve got a feed with information about certain cells (range An:Bn, where n is a row index). As the sheet contains 159 quotations, in order to provide a different one daily we compute that row index as the current day in year modulus total number of rows.

Last but not least, we need a hook; someplace in the blog template to anchor the newly retrieved content. We do so by adding to the template an HTML/Javascript module where we want our quip to appear. Its contents should be, at the very least,

<span class="cite"/>

(a <div> would also work —in fact, any container element with class cite). For other blogging platforms the same effect could be achieved by directly editing the template, or by some other means which I leave as exercise for the reader.

De reformas

Una de las mejores formas de conseguir que se implemente una característica en cualquier sistema informático en el que estemos interesados es, simplemente, arremangarse y codificarla. Quizá sea este el motivo fundamental por el que prefiero que el código que pase por mis manos sea abierto: dado interés y (más importante) tiempo suficiente, puede conseguirse cualquier cosa.

Filosofía de altos vuelos con un fin un tanto trivial. El widget de lista de etiquetas de Blogger es un tanto pobre. Si no se mantiene una bitácora monotemática, o si simplemente se es tan disperso como yo, se acaba enseguida con una lista interminable; y toda lista con más de veinte elementos es peor que nada. Veamos cómo modificar de forma simple la plantilla de Blogger para obtener algo mejor.

Advertencia: haz una copia de seguridad de tu plantilla antes de editarla. Si no lo haces, vendrá el hombre del saco y se llevará tu blog.

Asumo que en el blog a retocar hay un widget de lista de etiquetas estándar de Blogger en algún sitio (si no, inserta uno). Después, accede a la pestaña de edición de HTML de la plantilla y selecciona la caja correspondiente para expandir el código de los widgets. Busca el widget de la lista de etiquetas: normalmente, se llamará Label1 o algo similar. Tendrá este aspecto:

<b:widget id='Label1' locked='false' title='Etiquetas' type='Label'>
<b:includable id='main'>
  <b:if cond='data:title'>
    <h2><data:title/></h2>
  </b:if>
  <div class='widget-content'>
    <ul>
    <b:loop values='data:labels' var='label'>
      <li>
        <b:if cond='data:blog.url == data:label.url'>
          <data:label.name/>
        <b:else/>
          <a expr:href='data:label.url'><data:label.name/></a>
        </b:if>
        (<data:label.count/>)
      </li>
    </b:loop>
    </ul>

    <b:include name='quickedit'/>
  </div>
</b:includable>
</b:widget>

Observamos varias cosas:

  1. Parece XML estándar. Hay HTML, marcas con un espacio de nombres llamado b, y otras marcas de otro espacio llamado data.
  2. En el espacio de nombres b, vemos marcas “de programa”:
    • widget: delimitador de widgets. Claro…
    • includable: es una subdivisión interna del widget, direccionable desde fuera. Tiene que haber una al menos. No la vamos a tocar.
    • include: sirve para incluir un includable, que se distinguirá por nombre (id).
    • if: todos sabemos programar, ¿no? La condición va en el parámetro cond.
    • else: la otra rama del if.
    • loop: lo mismo de antes. El parámetro values especifica la colección por la que se iterará (es como un foreach de Perl o uno de los bucles nuevos de Java 5). El parámetro var da el nombre de la variable que tomará, en cada vuelta, el valor de uno de los elementos de la colección.

    Más información sobre las marcas de widgets aquí..

  3. El espacio de nombres data sirve para denotar elementos de la página. Algunas marcas están disponibles para toda la plantilla, mientras que otras sólo lo están dentro de widgets específicos. Más información sobre las marcas de datos, aquí.

Ya estamos preparados para retocar el widget de lista de etiquetas. Introduciremos un límite de frecuencia para mostrar la lista (por ejemplo, un mínimo de 5 ocurrencias), y relegaremos el resto a un combo. Será sencillo.

<b:widget id='Label1' locked='false' title='Etiquetas' type='Label'>
<b:includable id='main'>
  <b:if cond='data:title'>
    <h2><data:title/></h2>
  </b:if>
  <div class='widget-content'>
    <ul>
    <b:loop values='data:labels' var='label'>
      <b:if cond='data:label.count >= 5'>
      <li>
        <b:if cond='data:blog.url == data:label.url'>
          <data:label.name/>
        <b:else/>
          <a expr:href='data:label.url'><data:label.name/></a>
        </b:if>
        (<data:label.count/>)
      </li>
      </b:if>
    </b:loop>
    </ul>
    <br/>Otras: <select onchange='location.href=this.options[this.selectedIndex].value'>
    <b:loop values='data:labels' var='label'>
      <b:if cond='data:label.count < 5'>
        <b:if cond='data:blog.url == data:label.url'>
          <option value=''><data:label.name/> (<data:label.count/>)</option>
        <b:else/>
          <option expr:value='data:label.url'><data:label.name/> (<data:label.count/>)</option>
        </b:if>
      </b:if>
    </b:loop></select>
    <b:include name='quickedit'/>
  </div>
</b:includable>
</b:widget>

Básicamente, hemos cambiado la condición del if para no mostrar las etiquetas con frecuencia de aparición (data:label.count) inferior a 5. Incluimos otro bucle loop que excluye a las etiquetas procesadas anteriormente mediante una condición opuesta. Las etiquetas menos frecuentes se insertan en un combo de HTML con el código Javascript mínimo para navegar hacia ellas (dentro del evento onchange de la marca <select>).

Como mejora, tiene varios fallos. En particular, no cubre bien el caso de que haya pocos artículos. Controlar la frecuencia de aparición de las etiquetas puede llevar a mostrar una lista interminable con el tiempo, por lo que habría que tocar la plantilla de vez en cuando para aumentar la condición de corte entre la lista y el combo. Sería mejor controlar el número total de etiquetas. Y ya puestos, una interfaz moderna, tipo Web 2.0, con una caja de texto y lista dinámica de etiquetas coincidentes con lo tecleado sería ya para nota. Pero habrá que ver si hay tiempo, algún día.

How to blog in more than one language

The Spanish blogosphere looks weak. But language is accidental, automatic translation is ubiquitous, and the relative importance of content creator language is shrinking day in day out. However, when considering translation issues like those illustrated in the last post, perhaps some human intervention is in order to bridge the gap between semantic worlds.

To assist in this task, I have prepared a Blogger widget that allows writing posts in more than one language, discriminating among them and enabling the reader a clear view of her preferred content. To the user, the scheme resembles somewhat server-side content negotiation, but in a DOM-manipulating, Web-two-dot-zeroish way.

Some usage notes:

  1. First of all, you have to add the widget to your blog. There is a button next to the language selection combo to do just that. Not in Blogger? No problemo. Just download the script langsupport.js, and deploy it to your favorite location (it can be included in your template code, or anywhere in the HTML of your pages with a <script> tag, it is not too heavy, moreso if you use JS Minifier on it). You should at least remove the sentence with the comment Only needed when code is running inside a Blogger widget to make it work successfully outside Blogger.
  2. If not hosted on Blogger, you should also provide an anchoring point for Javascript to know where to insert the language selector combo. It should look like this:
    <span id="__langSupport__"></span>
  3. Denote language dependant HTML content with the lang attribute, using ISO 639-1 language names (there is no obligation to do so; however, the default language feature will malfunction). For instance:
    <p lang="en">This paragraph is in English.</p>
    <p lang="es">Este párrafo está en español.</p>
  4. Enjoy!

Things to keep in mind:

  • Unmarked HTML elements (those without lang attribute) will be left untouched.
  • A tag marked with a language which is not currently displayed will hide all its contents, even if some inside tag is marked with the currently displayed language. To avoid trouble, do not nest tags with different lang attributes.
  • If there are no lang attributes in the document, the combo selector will not appear.
  • The first language selected depends on default system locale. Therefore, if there is not content in that language on the page, the default language is selected (English, or ‘en’); if there is no content marked as the default language, the first one available (in lexicographical order) is chosen.
  • Unfortunately, the browser’s preferred language configuration is not exposed to the Javascript engine, AFAIK, so that setting cannot influence the default language here (it is only used for content negotiation with specifically configured servers): the default operating system locale is used instead.
  • This has been tested with Firefox 2. Never mind if IE also groks it.

Como bloguear en más de un idioma

La blogosfera española está triste. ¿Qué tendrá la blogosfera? El idioma es un accidente del destino, la traducción automática está por todas partes y la importancia relativa de la lengua del creador de contenidos disminuye cada día. Ahora bien, verter información de una lengua a otra puede ser aún una tarea que requiera de cerebros humanos, como vimos en la anterior entrada.

Para asistir en esa tarea, he preparado un widget de Blogger que permite escribir entradas en más de un idioma. Funciona como una negociación de contenido en servidor, pero en el lado del cliente. Además, usa esas técnicas de manipulación DOM que tan en boga están con la Web 2.0.

Un poquito acerca de su uso:

  1. Primero, tienes que añadir el widget a tu bitácora. Puedes usar el botón que aparece junto al combo de selección de idioma. Si no estás en Blogger, bájate el script langsupport.js, y despliégalo en tu ubicación favorita (puede ir incluido en tu plantilla o en el código HTML de la página que lo use con una marca <script>, es pequeño, y más aún si lo pasas por JS Minifier). Al menos tendrás que eliminar la línea marcada como Only needed when code is running inside a Blogger widget para que funcione fuera de Blogger sin problemas.
  2. Si estás fuera de Blogger, tendrás que añadir también un “ancla” para que el código Javascript sepa dónde colocar el combo selector de idiomas. Eso tiene esta pinta:
    <span id="__langSupport__"></span>
  3. Denota tu contenido con marcas HTML que usen el atributo lang, usando nombres de idioma según el estándar ISO 639-1 (en teoría podrías poner cualquier cosa, pero entonces la funcionalidad de idioma por defecto se vería afectada). Por ejemplo:
    <p lang="en">This paragraph is in English.</p>
    <p lang="es">Este párrafo está en español.</p>
  4. ¡Disfruta!

Algunas notas importantes:

  • Aquellos elementos HTML que no estén marcados (los que no tengan atributo lang) no serán modificados.
  • Cualquier marca que lleve un idioma que no esté seleccionado ocultará todos sus contenidos, incluso si otra marca anidada lleva el idioma actual. Para evitar problemas, no anides marcas con diferentes valores del atributo lang.
  • Si no hay atributos lang en todo el documento, el selector de idioma no aparecerá.
  • El primer idioma seleccionado depende del idioma por defecto del sistema operativo. Si no hay contenido para ese idioma en la página, se selecciona un idioma por defecto (el inglés). Si no hay contenido en inglés, se muestra el contenido del primer idioma disponible, en orden alfabético.
  • Desafortunadamente, la configuración de idioma preferido por el navegador no está disponible para Javascript (sólo se usa en la negociación de contenidos con servidores que dispongan de esa posibilidad). El idioma por defecto es el del sistema operativo.
  • He probado esto en Firefox 2. Si también funciona en IE, pues vale.