jueves, 12 de septiembre de 2013

Terminamos el proyecto

Esto se va acabando


Sólo nos queda un método. El dichoso “findEndOfTag” que nos indica donde acaba un elemento. Para ello tenemos que pasarle como parámetro la posición en que comienza dicho elemento:

def findEndOfTag(start)
  if @nodes == nil
   raise 'Not initialized'
  end

  lasttag = @nodes.length - 1
  ending = lasttag

  if @nodes[start].class.name != "HTMLTag"
   ending = start
  elsif isClosed?(start)
   ending = start
  else
   tname = @nodes[start].name
   tagsfound = 0
   (start..lasttag).each do |x|
    if @nodes[x].class.name == "HTMLTag"

       and @nodes[x].name == tname
       and not isClosed?(x)
         tagsfound += 1
    elsif @nodes[x].class.name == "HTMLClose" and @nodes[x].name == tname
         tagsfound -= 1
    end

    if tagsfound == 0
     ending = x
     break
    end
   end
  end


  return ending

end

Para empezar, asumimos que, si no demostramos otra cosa, el objeto acabará al final del documento:

lasttag = @nodes.length - 1
ending = lasttag

Ahora bien, los elementos que no son etiquetas HTML sólo tienen un nodo. O sea: acaban donde empiezan. Y lo mismo ocurre con las etiquetas HTML que, como mencionamos anteriormente y por una u otra razón, se autocierran:

if @nodes[start].class.name != "HTMLTag"
  ending = start
elsif isClosed?(start)
  ending = start

En otro caso, miraremos uno a uno los nodos a partir de la apertura de la etiqueta. Hay que tener en cuenta que podemos encontrarnos con etiquetas del mismo nombre de forma anidada, como en:

<div>
abcde <div id=abcde> HOLA </div>
</div>

… así que anotamos el nombre de la etiqueta cuyo final andamos buscando y llevamos la cuenta de cuántas etiquetas de apertura no autocerradas y cuántas etiquetas de cierre vamos encontrando con el mismo nombre.

tname = @nodes[start].name
tagsfound = 0
(start..lasttag).each do |x|
  if @nodes[x].class.name == "HTMLTag"
    and @nodes[x].name == tname
    and not isClosed?(x)
      tagsfound += 1
  elsif @nodes[x].class.name == "HTMLClose" and @nodes[x].name == tname
      tagsfound -= 1
  end



Cuando el número de aperturas y de cierres sea el mismo hemos encontrado el final y podemos salir del bucle. Precisamente para eso es para lo que sirve la instrucción “break”:

if tagsfound == 0
  ending = x
  break

Y tras cerrar con sus correspondientes “end” todo lo que hemos ido abriendo, retornamos la posición en que acaba la etiqueta:

 
return ending

 

Chin pon


¡Eh! ¡Espera!

 
Aún no hemos acabado. Que no se nos olvide poner un último “end”. El que cierra la definición de la clase:

 
end # class HtmlDoc


Ahora sí. Ya podemos sacar conclusiones.

La clase HTMLDoc supone un paso más de abstracción en la representación de documentos HTML. Un paso que, gracias al paradigma de la programación orientada a objetos, se aprovecha del trabajo que ya habíamos hecho con clases como HTMLParser.


Ahora podremos hacer programas más resultones. Y nos costará mucho menos trabajo.

Aunque, claro, está muy bien eso de poder gestionar el código HTML, pero... ¿de donde lo sacamos? Creo que estaréis de acuerdo en que es de los servidores web de donde uno suele conseguir código HTML más frecuentemente. Así que necesitaremos una clase que interactúe con ellos.

Pero eso es otra historia








miércoles, 11 de septiembre de 2013

La reproducción de los objetos

La reproducción de los objetos


En el colegio me enseñaron que “los seres vivos nacen, crecen, se reproducen y mueren”. Por lo visto los HTMLDoc son seres vivos. Nacen cuando los crea una llamada a “new”. Crecen cuando les añadimos código HTML con cosas como “insertAfter” o “insertBefore”. Mueren cuando se les hace una llamada a “clear” cuando eliminamos nodos.

Y se reproducen, porque un objeto puede generar otros de su misma clase mediante “getElement”. Que es el único método que me queda por pagaros. Así que ahí va:

 
def getElement(start)
  if not (0..(@nodes.length - 1)).include?(start)
   raise 'Element out of range'
  end

  ending = findEndOfTag(start)

  new_element = HtmlDoc.new
  new_element.type = :node
  new_element.nodes = @nodes
  new_element.starts_at = start
  new_element.ends_at = ending
  new_element.document = typeOf == :document ? self : @document

  new_element.document.nodes_created.push(new_element)
  return new_element
end

 
Le pasamos un número con la posición en que comienza el código HTML del objeto que queremos retornar y “getElement” hace el resto. Primero comprobamos que la posición que nos dan es correcta. Si todo va bien, seguiremos con

ending = findEndOfTag(start)

… que asigna a “ending” la posición en que acaba el código HTML del objeto. Y para ello llamamos al método “findEndOfTag”... que aún no hemos definido. ¡Jolín! ¡Ahora que estábamos casi acabando y nos aparece otro método que dejaremos para más adelante!

Tranquis, que tampoco es para tanto, y sigamos. Creamos un nuevo objeto de la clase HTMLDoc para representar el elemento:

new_element = HtmlDoc.new

… e inicializamos sus valores, par decir que es un nodo (no un documento completo), que comparte los nodos de su padre (que en algo se tenía que parecer a él), indicar donde comienza y acaba y señalar al documento al que pertenece:

new_element.type = :node
new_element.nodes = @nodes
new_element.starts_at = start
new_element.ends_at = ending
new_element.document = typeOf == :document ? self : @document

Fijaos en que las invocaciones a “self” o “typeOf” se refieren al objeto padre, sobre el que estamos invocando el método, no al hijo que estamos creando. Y recordad que podemos modificar un objeto desde otro porque definimos los accesores como “protegidos”.

Y otra cosa: en la asignación "new_element.nodes = @nodes" hay que tener en cuenta que cuando asignamos a una variable un objeto en Ruby NO creamos una nueva copia del objeto. De modo que el atributo @nodes del nodo que acabamos de crear y el de su padre harán referencia a un mismo objeto.

Como prueba valga el siguiente ejemplo:

$ irb
irb(main):001:0> a = [1,2,3]
=> [1, 2, 3]
irb(main):002:0> b = a
=> [1, 2, 3]
irb(main):003:0> a[2] = 1     # Modificamos el array original
=> 1
irb(main):004:0> b
=> [1, 2, 1]
irb(main):005:0> # ... y b también ha sido modificado

Para crear copias de un objeto tenemos el método clone

irb(main):006:0* c = a.clone    # c es ahora una copia de a
=> [1, 2, 1]
irb(main):007:0> a[1]=100    # Si modificamos a...
=> 100
irb(main):008:0> c 
=> [1, 2, 1]
irb(main):009:0>  # c sigue igual

Sigamos. Al documento original hay que decirle que hemos creado un nuevo elemento a partir de él, para que vaya llevando la cuenta:

new_element.document.nodes_created.push(new_element)

Y... listo. Sólo queda retornar el nuevo elemento creado:

 
return new_element

 

viernes, 30 de agosto de 2013

Sólo para ti

Para ti y sólo para ti

Y, para que nadie diga que me escabullo de mis promesas con la facilidad de un pez, comencemos ya con los métodos privados. Lo primero es una línea que marque claramente el territorio. Ponemos:

private

... y a partir de aquí, lo que pongamos será privado. Sólo accesible por el propio objeto.

Comencemos con el método “tagRange”, que nos proporciona el rango de los nodos que representan el objeto:

def tagRange
  if typeOf == :deleted
   raise "Deleted"
  end
  return (@starts_at .. @ends_at)
end

Recuerda que con “..” creábamos rangos. Así:

5..9

… se corresponde con el rango de números enteros formado por 5, 6, 7, 8 y 9.

Éste ha sido muy sencillete. Veamos ahora cómo nos las ingeniamos con “nodesHTLM”, que recibe como parámetro un rango y nos genera una cadena de texto con el contenido HTML de los nodos comprendidos en ese rango:

def nodesHTML(range)
  return range.collect{|x| @nodes[x].to_s}.join
end

Tampoco es que haya sido muy complicado. Resolvamos otro problema. Sabemos que una etiqueta HTML puede contener su propio “cierre” y no tener contenido interno, como en:

<div id="prueba" />

La barra “/” que precede al “>” indica que no tenemos que andar buscando una etiqueta de cierre del tipo “</div>”. Pero es que hay algunas etiquetas, como “<img ...>” o “<br>” que no necesitan etiqueta de cierre. Se cierran ellas solas, incluso si no les ponemos la barra al final. Para darles un tratamiento “personalizado”, vamos a crear un método que nos detecte si el objeto actual representa una de estas etiquetas o si incluye de forma explícita la barra al final para autocerrarse:

def isClosed?(start=@starts_at)
  if @nodes[start].class.name != "HTMLTag"
   raise 'Not a tag'
  end

  return (@nodes[start].autoclosed or
   ['meta','link','img','br','hr','wbr','input','source','param','frame',
   'keygen','area','base','batsefon','col','embed','bgsound'].include?

      (@nodes[start].name.downcase))
end

El método crea un array con los nombres de las etiquetas que se cierran ellas solitas incluso sin barra y comprueba si el nombre de la etiqueta representada por el objeto está en dicho array. Sencillo a la par que elegante. Y por si queremos comprobar otros nodos, podemos pasarle un parámetro.

Eso sí, tened en cuenta que la lista de nombres de etiquetas no es exhaustiva. Seguro que alguna se me olivda.

Pasemos ahora al que quizá sea el método más complejo de la clase: “deleteTags”.

Nosotros le pasamos un rango y él elimina los nodos correspondientes a ese rango. Vaya primero el código

def deleteTags(range)
  if range.count == 0
   return
  end

  doc_range = (0..(@nodes.length - 1))
  if not doc_range.include?(range.first) or not doc_range.include?(range.last)
   raise 'Out of range'

  end

  # Delete range
  @nodes.slice!(range)

  # Fix top document count
  document = @type == :document ? self : @document
  document.ends_at = @nodes.length - 1

  # Fix nodes created from top document
  document.nodes_created.each do |n|

   if n.starts_at >= range.first
    if n.starts_at <= range.last
     # Object is inside deleted range. Clear it
     n.clear
    else
     # Object is outside deleted range. Keep it, but...
     # Compute new start and end nodes
      n.starts_at -= range.count
     n.ends_at -= range.count
    end
   

   elsif n.ends_at >= range.first
    if n.ends_at <= range.last
     # "Non well formed" HTML: trim elements
     n.ends_at = range.first - 1
     if n.ends_at < 0
      n.clear
     end
    else
     # The deleted range is inside the node
     # Compute new end node
     n.ends_at -= range.count
    end
   end
  end
end
  

La primera parte del método hace unas cuantas comprobaciones y elimina los nodos:

...
...
if not doc_range.include?(range.first) or not doc_range.include?(range.last)
  raise 'Out of range'
end

# Delete range
@nodes.slice!(range)

El método “slice!” elimina un rango de elementos de un array. Y, por si necesitamos saber qué había en los elementos eliminados, retorna un array con lo que se ha cargado. Existe un método parecido llamado “slice” que funciona de forma similar, pero sin borrar nada del array original.

Hasta aquí todo bien... excepto por los “efectos laterales”. Vamos a ver: imaginemos que hemos ido creando elementos a partir del documento con llamadas, por ejemplo, a “getElementById”. Al quitar código HTML de nuestro documento puede que algunos de estos elementos desaparezcan. O que varíen sus posiciones de inicio o final, puesto que hemos quitado cosas.

Así que ahora tenemos que arreglar el desaguisado. Comencemos por el documento padre, el original.

El objeto que invoca el método puede ser o no el documento original, así que primero lo comprobamos:

document = @type == :document ? self : @document

… y corregimos su atributo @ends_at

document.ends_at = @nodes.length - 1

Recuerda que podemos hacer esto porque el accessor a @ends_at es de tipo “protegido”.

Y ahora le damos un repaso a todos los nodos creados a partir del documento. “document”, el objeto que lo representa los almacena en un atributo llamado “nodes_create”. Así que se hace un bucle sobre ellos con:

document.nodes_created.each do |n|
...
...
...

end

Dentro de este bucle, para cada nodo “n” realizamos las siguientes comprobaciones:

  1. Si el inicio del nodo está dentro del rango del nodo, eliminamos dicho nodo mediante una llamada a “clear”. Y a otra cosa, mariposa.
  2. Si el objeto comienza después del final del rango eliminado, tenemos que restar de sus índice de inicio y finalización el número de nodos que hemos borrado.
  3. Si el objeto comienza antes del inicio del rango borrado y acaba dentro de él, habrá que corregir su índice de finalización y decir que acaba justo antes del rango que acabamos de eliminar.
  4. Si el objeto comienza antes del inicio del rango eliminado y acaba fuera de él, o sea si el objeto contiene dentro el rango eliminado, habrá que corregir sólo su índice de finalización, decrementándolo en el número de nodos eliminados.
O, dicho en Rubyense:

if n.starts_at >= range.first
  if n.starts_at <= range.last
    # Object is inside deleted range. Clear it
   n.clear
  else
   # Object is outside deleted range. Keep it, but...
   # Compute new start and end nodes
   n.starts_at -= range.count
   n.ends_at -= range.count
  end
elsif n.ends_at >= range.first
  if n.ends_at <= range.last
   # "Non well formed" HTML: trim elements
   n.ends_at = range.first - 1
   if n.ends_at < 0
    n.clear
   end
  else
   # The deleted range is inside the node
   # Compute new end node
   n.ends_at -= range.count
  end
end

Añadir nodos es mucho más sencillo que borrarlos. La única corrección necesaria consiste en incrementar los inicios y finales de nodos que se produzcan después del punto en que insertamos el nuevo contenido. Eso es lo que hace “inserTags”, que añade en una posición dada un conjunto de nodos o etiquetas:

def insertTags(position,tags)

  # Add the new elements
  if position >= @nodes.length
   # Append
   @nodes.concat(tags)
  elsif position >= 0
   @nodes.insert(position, tags).flatten!
  else
   raise 'Invalid position'
  end

  # Fix top document indexes
  document = typeOf == :document ? self : @document
  document.ends_at += tags.length

  # Fix nodes created from top document
  document.nodes_created.each do |n|
   if n.starts_at >=position
    n.starts_at += tags.length
   end

   if n.ends_at >=position
    n.ends_at += tags.length
   end
  end
end
  

 

lunes, 26 de agosto de 2013

Lo que el documento esconde

Lo que el documento esconde


¡Bonito título! ¿Verdad? Y lo mismo sirve para un artículo sobre los METADATOS que para un post sobre Ruby.

Hasta aquí hemos visto los métodos que nuestra clase HTMLDoc expone cara al exterior. Digamos que “lo que ve la suegra”. Pero, como vimos en el proyecto anterior (http://elrubymola.blogspot.com.es/2013/02/compare-compare-2-de-2.html) las clases pueden tener también métodos privados, que sólo el propio objeto puede invocar.

Pero eso no termina de resolver nuestros problemas de hoy.

Os cuento: A lo largo de los anteriores epígrafes he ido dejando no sé ya cuántos métodos “para más adelante”. Y aún no los hemos desarrollado. Ya habrá quien diga “este tío es un mal pagador” o “no es de fiar” o “mucho prometer, pero...”.

Como no quiero quedar mal, y como no es algo que cueste dinero, voy a ponerme con ello. Interesante, eso de estudiar las “estrategias para parecer un tipo honesto”.

Vayamos al tajo y pensemos en el método “getElement”, uno de los que tenemos que escribir aún. Recibe un parámetro numérico y genera un objeto de tipo HTMLDoc que representa el elemento del documento que comienza en la posición indicada por dicho número.

Pensemos, que es gratis: El método no debe ser público porque no aporta nada a la funcionalidad de la clase y trata con temas de cómo representamos a nivel interno los datos. Pero tampoco puede ser privado, porque los métodos privados sólo pueden ser invocados por el propio objeto.

Y se trata de que un objeto HTMLDoc pueda crear otro y modificar su contenido interno.

Ruby tiene una solución: los métodos protegidos. Si un objeto tiene un método protegido, éste puede ser invocado por todos los objetos de su misma clase. ¡Justo lo que necesitábamos! Y para empezar a definir métodos protegidos basta con poner la palabra “protected”. Todo lo que pongamos a continuación estará “protegido”, mientras no cambiemos de modo utilizando “public” o “private”.

Vamos a hacer dos cosas:
  1. Definir unos accesores a los atributos del objeto. De ese modo, cuando un objeto genere otro con “getElement”, podrá asinarle el contenido que sea pertinente.
  2. Definir un nuevo método que vacía el contenido de un objeto y lo marca como eliminado. Aún no nos ha hecho falta, pero ya lo utilizaremos más adelante, cuando quitemos contenido del documento.

Ahí van ambas cosas de un tirón:

protected
# Parent Document/Tag may use these to create objects representing its descendants
attr_accessor :type, :nodes, :starts_at, :ends_at, :document, :nodes_created

# Clears data from a deleted object
def clear
  @type = :deleted
  @nodes = nil
  @starts_at = -1
  @ends_at = -2
  @document.nodes_created.delete_if{|x| x.equal?(self)}

  @document = nil
end

Observad como el método clear elimina el objeto de la lista de nodos creados a partir del documento original haciendo uso de “delete_if”.

sábado, 24 de agosto de 2013

Cambiando el documento

Cambiando el documento

Hasta aquí hemos consultado el documento y su contenido. Lo cual está muy bien. Pero... ¿y si quiero hacer cambios?

Vayamos por partes. Comencemos por un método que permita eliminar un nodo, junto con sus posibles nodos hijos:

def removeFromDocument
  if typeOf == :document
   raise 'Cannot remove the whole document'
  end

 
deleteTags(tagRange)
end

Mal empezamos. Este método llama a otro que dejaremos para más adelante. ¿Cuántos os debo ya? Uno, dos, tres, cuatro...

Pero si lo vamos a dejar para más adelante, lo dejamos y seguimos. Que si no nos atrancamos. Que, como bien apuntan los Monty Python, ya se sabe que hay que hablar menos y hacer más. Porque sólo diciendo no se avanza y es preciso actuar. Porque lo importante es lo que se hace y habría que dejar a un lado la palabrería para centrarnos en lo importante...

¿Por dónde íbamos? Eso... vamos a crear un método que nos permita cambiar el “innerHTML” de una etiqueta. Para ello, primero miramos si la etiqueta no cuenta con un cierre explícito y, si es necesario, lo añadimos. Después, eliminamos los nodos correspondientes al “innerHTML” antiguo. Y finalmente insertamos en su lugar lo nuevo:

def changeInnerHTML(html)
  if typeOf != :HTMLTag
   raise 'Not a tag'
  end

  # If there is no closing tag, add it
  
if @nodes[@starts_at].autoclosed
   insertTags(@starts_at + 1, [HTMLclose.new(tagName)])
   @ends_at = @starts_at + 1
   @nodes[@starts_at].autoclosed = false
  end
   

  deleteTags((@starts_at + 1)..(@ends_at - 1))
   

  insertTags(@starts_at + 1, HTMLParser.new.parseHTML(html))
end

Para las inserciones, realizamos llamadas a otro método, “insertTags”. Y sabéis qué: voy a dejar éste también para más adelante... Por ahora sólo diré que inserta, en la posición que le indiquemos como primer parámetro, el código HTML que le pasemos como segundo.

Utilizando “insertTags” podemos crear otros dos métodos que permitan insertar código HTML antes o después del objeto que estamos manipulando:

def insertBefore(html)
  insertTags(@starts_at, HTMLParser.new.parseHTML(html))
end

def insertAfter(html)
  insertTags(@ends_at + 1, HTMLParser.new.parseHTML(html))
end

miércoles, 7 de agosto de 2013

Extraer información

Los atributos

Aunque ya tenemos definido el método llamado “attributes”, no viene mal tener unos cuantos más para consultar o modificar los contenidos de los atributos. 

Para ello podemos utilizar los operadores de consulta y asignación de índices. Así, para consulta el valor de un atributo tendremos:

def [](index)
  if typeOf != :HTMLTag
   raise "Not a tag"
  end

  return @nodes[@starts_at].attributes[index]
end

Y, para asignarle un valor:

def []=(index, value)
  if typeOf != :HTMLTag
   raise "Not a tag"
  end

  
@nodes[@starts_at].attributes[index] = value
end

Sacando cosas de un documento

Hasta aquí tenemos forma de crear un documento y de obtener información del mismo.

Y también de los elementos que se puedan crear a partir de él. Lo cual estaría muy bien, si no fuera porque aún no sabemos como crear estos elementos.

Eso... ¿Cómo creamos los elementos a partir de un documento?

Bien... buena pregunta... Comencemos por ver como podemos obtener un elemento del documento a partir de su atributo “ID”. Para eso, cuando manipulamos HTML mediante scripts, se suele contar con un método llamado “getElementById”, aplicable a documentos y etiquetas. Su implementación en Ruby para nuestra clase sería:

def getElementById(id)
  if typeOf != :HTMLTag and typeOf != :document
   raise "Not a tag"
  end

  
tagRange.each do |x|
   if @nodes[x].class.name == "HTMLTag" and @nodes[x].attributes['ID'] == id
    return getElement(x)
   end

 end

  return nil
end
  

El método recorre con la variable “x” el rango de nodos correspondiente al objeto actual y, para cada uno de ellos comprueba si se trata de una etiqueta HTML y, de ser así, si su atributo “ID” es igual al que estamos buscando.

Cuando se cumplen estas condiciones, es que hemos dado con lo que estamos buscando. Sólo queda retornar la representación del objeto que comienza en la posición “x” actual.

Pero para eso utilizamos otro método, “getElement” que vamos a dejar para más adelante.

Y con eso van tres métodos que os debo.

Otra cosa interesante sería poder obtener un array, por ejemplo, de todas las etiquetas “<DIV>” de un documento. O de todos los “<li>” subordinados a un determinado “<ul>”. Para cosas como esas podemos definir el siguiente método:

def getElementsByTagName(thetagname)
  return tagRange.select{|x| @nodes[x].class.name == "HTMLTag"

     and @nodes[x].name == thetagname}.collect{|e| getElement(e)}
end

Básicamente, con el “select” miramos dentro del rango de nodos del objeto actual y creamos un array de los valores de x en que aparece una etiqueta con el nombre dado. Posteriormente, a partir de este array, con el “collect” generamos un array de los elementos que empiezan en dichos valores.

Algo parecido podemos hacer para obtener un array de aquellas etiquetas que tengan un determinado atributo igual a un valor dado:

def getElementsByAttribute(attribute, value)
  return tagRange.select{|x| @nodes[x].class.name == "HTMLTag"

     and @nodes[x].attributes[attribute] == value}.collect{|e| getElement(e)}
end

Para ir acabando con esto de generar elementos, podemos crear un array de los nodos hijos de una etiqueta dada o del documento completo como sigue:

def children
 range = tagRange
 
  last_tag = range.last + (typeOf== document ? 1 : 0)
 x = typeOf == :document ? 0 : (range.first + 1)
  result = []
  while x < last_tag
   elemento = getElement(x)
   result.push(elemento)
   x = elemento.ends_at + 1
 end

   
 return result
end
  

Dicho en román paladino: mira dentro de lo que se correspondería con el “innerHTML” y cada vez que encuentres un nodo, añádelo a la lista de resultados, mira donde termina su definición, y continúa buscando a partir de ahí.

lunes, 5 de agosto de 2013

El contenido de los nodos

El contenido de los nodos

Una vez que tengamos un documento o uno de sus elementos, quizá queramos saber qué tiene dentro. Tanto su código HTML como el texto que pudiera contener.

En cuanto al código HTML podemos distinguir entre HTML interno y externo. Si nos fijamos en la etiqueta “<div>” del siguiente ejemplo:

<div>
<a href="#asd">Prueba</a>
</div>

… su HTML interno es lo que hay dentro del <div>. Así, quitándole la cáscara a nuestra etiqueta nos queda:

<a href="#asd">Prueba</a>

… mientras que su HTML externo es todo el código fuente correspondiente a la etiqueta <div>:

<div>
<a href="#asd">Prueba</a>
</div>

Y, en lo que al texto de la etiqueta respecta, tendríamos sólo:

Prueba

Para conseguir el HTML externo definiremos el método outerHTML:

def outerHTML
  if typeOf == :deleted
   raise "Deleted"
  
end
  return nodesHTML(tagRange)
end

Primero miramos si el nodo no ha sido eliminado, porque si sí lo ha sido, no hay nada que mirar. Si todo está OK, llamamos a otro método que definiremos más adelante llamado “nodesHTML” que lista el contenido de unos cuantos de los nodos que componen el documento principal.

¿Que cuáles nodos lista? Bueno, eso lo indicamos mediante el parámetro que le pasamos, que debe ser un rango. Y, en este caso, ese rango es el resultado de “tagRange”, otro método que dejamos para más adelante y que nos proporciona el rango de nodos que componen el elemento actual.

Ya os debo dos métodos.

También vamos a crear el método “to_s” como un alias para “outerHTML”.

alias_method :to_s, :outerHTML

Del HTML interno sólo tiene sentido hablar si estamos ante una etiqueta o bien de un documento.

def innerHTML
  if typeOf == :document
   return outerHTML
  
elsif typeOf != :HTMLTag
   raise 'Not a tag'
  end

  range = tagRange
  return nodesHTML((range.first + 1) .. (range.last - 1))
end

Finalmente, para el contenido de texto de un nodo habría que seleccionar aquellos elementos de tipo “texto” que le pertenezcan y concatenar sus contenidos. Para quienes sólo tengan interés por la palabrería pura y dura, su método preferido será:


def textContent
  if typeOf == :deleted
   raise "Deleted"
  
end
  return tagRange.select{|x| @nodes[x].class.name == 'HTMLText'}.collect{|t|
       @nodes[t].to_s}.join.to_s
end

martes, 30 de julio de 2013

Primeros pasos

El primer paso

Como puede observarse, nuestro fichero comienza incluyendo al de la clase HTMLParser. Y es que no vamos a repetir otra vez todo lo que hicimos ahí. Otra cosa buena de la programación orientada a objetos: poder reutilizar nuestro código.

Comencemos por el constructor de la clase:

def initialize(html=nil)
  # HTML Document tags (array of HTMLThing objects)
  @nodes = html == nil ? [] : HTMLParser.new.parseHTML(html)

  # All objects created from a document share the @nodes attribute

  # Type (:document, :deleted, :node)
  @type = :document

  # Starting and ending indexes in @nodes for current object
  @starts_at = 0
  @ends_at = @nodes.length - 1


  # Pointer to Document the current object was generated from
  @document = nil

  # For :document objects, an array of objects created from it
  @nodes_created = []
end

Poco nuevo por aquí. Utilizamos un HTMLParser para analizar el código HTML que nos pasan como argumento e inicializamos las variables del objeto con unos valores razonables. A ver qué podemos hacer ahora con esto.

Una cosa interesante sería poder saber cuando nos dan un objeto de esta clase es saber si se trata de un documento, de un elemento eliminado, de si representa una etiqueta HTML, un nodo de texto, etc. Recordaréis que las clases para estos últimos tipos de objetos HTML las definimos en su día en el fichero “htmlthings.rb”.

¡Dicho y hecho! Aquí está el método que lo hace:

def typeOf
return (@type == :document or @type == :deleted) ? @type :
@nodes[@starts_at].class.name.to_sym
end

Las construcciones del tipo

Condición ? Expresión_1 : Expresión_2

… vienen a significar: “Si Condición_1 es cierto, evalúa Expresión_1. En caso contrario, evalúa Expresión_2”. Algo así como el “IF inmediato” de algunos lenguajes.

De modo que para los :document y los :deleted se devuelve su tipo. Para los :node, hay que mirar en qué posición del array de nodos comienza el contenido del objeto. Ése elemento es el que en realidad nos indica qué tenemos entre manos.

Con “.class.name.to_sym” tomamos el nombre de la clase del objeto y lo convertimos en un Symbol. ¡Y listos!

O casi listos. Porque si tenemos una etiqueta HTML quizá necesitemos saber su tipo: si es una “IMG”, un “DIV”, una “LI”, etc. Pero eso es fácil de resolver con otro método:

 
def tagName
  if typeOf != :HTMLTag
   raise 'Not a tag'
  end


  return @nodes[@starts_at].name.downcase
end

Y si queremos obtener un hash con sus atributos tampoco lo tenemos mucho más difícil:

def attributes
  if typeOf != :HTMLTag
   raise 'Not a tag'
  end


  return @nodes[@starts_at].attributes
end

viernes, 26 de julio de 2013

Andando, que es gerundio

Andando, que es gerundio

Quizá a alguien se le ocurra alguna representación mejor para los datos. Seguro que sí. Pero eso es lo bueno que tiene la programación orientada a objetos: más adelante podríamos cambiar como funcionan nuestros “HtmlDocs” por dentro y, mientras mantengamos las mismas interfaces públicas, externas, no sería necesario modificar el resto de nuestras aplicaciones.

Comencemos por definir la parte visible de nuestra clase. Únicamente sus métodos públicos vacíos, como viene siendo costumbre. Que nadie se preocupe, que más adelante los rellenamos:

require File.dirname(__FILE__) + '/htmlparser.rb'
# Class for HTML Document and object management
# HTML Docs keep track of nodes created from them
# nodes with HTML elements can be managed just like documents

  
class HtmlDoc
# Object creation
def initialize(html=nil)
end

###################
# Node information
###################

# Returns the type of the Html Doc/Node object
def typeOf
end


# Name of the tag represented by the object
def tagName
end
# Hash of attributes of the element represented by the object
def attributes
end

###########################
# HTML and Text extraction
###########################

# HTML code for the object, including its own tags
def outerHTML
end

# to_s is just another alias for outerHTML
alias_method :to_s, :outerHTML

# HTML code for the object, excluding its own tags
def innerHTML
end


#############
# Attributes
#############

# Get attribute value
def [](index)
end

# Set attribute value
def []=(index, value)
end

################################
# Element search and navigation
################################

# Get a element, given its Id
def getElementById(id)
end

# Get an array of elements with a given tag name
def getElementsByTagName(thetagname)
end


# Array of elements with an given attribute being equal to a given value
def getElementsByAttribute(attribute, value)
end

# Array of child nodes for an element
def children
end


#############################
# Document/Node manipulation
#############################

# Delete current node from document
def removeFromDocument
end

# Change innerHTML for an element. Doesn't modify the element itself,
# but the ones inside of it.
def changeInnerHTML(html)
end

# Insert HTML before a node
def insertBefore(html)
end

# Insert HTML after a node
def insertAfter(html)
end
end

Guardemos esto en nuestra carpeta de siempre con el nombre “htmldoc.rb”.

Ya nos hemos puesto en marcha.

Continuará...

martes, 23 de julio de 2013

Intro

Intro: En episodios anteriores de “Aprendiendo Ruby a fuerza de cabezazos”

Han pasado muchas lunas ya desde la última vez que visité este garito. Pero no creáis que he estado perdiendo el tiempo desde entonces.

Bueno, quizá sí. Os cuento.

Lo último que recuerdo es que había terminado un proyecto y lo había publicado todo en este blog. Y que consistía en unas cuantas clases de Ruby que me servían para analizar un fichero en formato HTML y convertirlo en un array de objetos relativamente manejables.

Pero en mi cofre del tesoro había algo más. Unos cuantos folios en los que creaba una nueva clase mucho más potente. El único problema es que, sin saber cómo, se me había caído el contenido de una botella de ron sobre ellos y la tinta se había disuelto.

Ilegible. Y mi providencial memoria, como era de esperar, no recordaba un solo punto y coma de aquello. Así que me puse y dispuse a intentar descifrar, casi diría adivinar, qué había escrito yo allí. No fue fácil, con aquel aroma a alcohol seco siempre presente.

Siempre presente...

Quizá lo que voy a describir en éste y los siguientes posts no tenga mucho que ver con el contenido original de aquellos documentos primigenios. Pero es cuanto fui capaz de sacar en claro, dadas las circunstancias.

El desafío

Eso de tener un array con las etiquetas y elementos del documento HTML está muy bien. Pero es francamente mejorable. Uno querría acceder al documento y su contenido de forma similar a como lo hace habitualmente en JavaScript o Visual Basic Script. Con instrucciones del tipo:

document = HtmlDoc.new(File.read('test.html'))
a = document.getElementById('menu')
opciones = a.getElementsByTagName('div')
print opciones[1].attributes['onmouseover']

Así sí que daría gusto trabajar con documentos HTML. Y si además podemos introducir cambios en el documento, entonces sería para premio.

La idea

Y no es tan difícil. Únicamente vamos a necesitar crear una nueva clase cuyas instancias representarán o bien el documento HTML completo, o bien uno de los elementos y otros nodos que lo componen.

La primera idea que podría venírsele a uno a la cabeza sería crear una estructura en árbol que represente el documento. Y eso estaría muy bien si se tratara de analizar XML. Pero HTML y los programas que lo manejan son muy... permisivos. De forma que cosas que en XML serían intolerables como etiquetas mal anidadas...

<B><U>Vaya con el HTML</B></U>

… pues vienen los navegadores web y se las tragan sin rechistar.

Así que había que buscar alguna alternativa. La que se me ocurrió no es seguramente la mejor, pero funciona (al menos, relativamente bien). Vayamos por partes.

Los objetos tendrán un atributo @nodes en el que se copiará el resultado de analizar el documento HTML con un HTMLParser de los que creamos en su día. Ese atributo @nodes estará compartido por el objeto que representa el documento y por todos los elementos y nodos que puedan generarse a partir de él (por ejemplo, con getElementById).

Cada objeto necesitará saber también si es un documento original o un nodo generado a partir de él. Para eso habrá otro atributo llamado @type que podrá tomar los valores :document y :node. Además, existirá un tercer valor, :deleted, para identificar los nodos que puedan haber sido eliminados como resultado de la manipulación del documento.

Cada objeto hará referencia a una parte del documento o bien al documento completo. Para determinarla tendremos dos índices, @starts_at y @ends_at, que indican su inicio y su final dentro del array de nodos @nodes.

Para los nodos que no representen etiquetas HTML, @starts_at y @ends_at contendrán el mismo valor. Pero si una etiqueta tiene otras cosas dentro, la cosa puede cambiar.

Por ejemplo, si tenemos un documento del tipo:

Posición Nodo representado
0 <div>
1 <ul>
2 <li>
3 Un elemento
4 <li>
5 Otro elemento
6 </ul>
7 </div>

… podemos ver que la etiqueta “<ul>” comienza en la posición 1 y que su contenido se extiende hasta la posición 6, donde se cierra con “</ul>”. De modo que, para un objeto que represente esta etiqueta, @starts_at debe valer 1; y @ends_at, 6.

Para ir acabando, cada nodo generado a partir de un documento dado mantendrá una referencia al mismo mediante el atributo @document. Y el documento tendrá un array con enlaces a los nodos creados a partir de él, @nodes_created. Ya veremos para qué.

Aunque seguro que hay quien se hace ya una idea.

martes, 5 de marzo de 2013

Utilizando lo que hemos hecho

Bueno. Ya tenemos nuestra clase para analizar código HTML. Pero... ¿para qué sirve?

Pues entre otras cosas, para poder procesar archivos HTML de forma sencilla.

Pongamos un ejemplo: es frecuente que los documentos HTML hagan referencia a datos almacenados en otros ficheros. Como una etiqueta “IMG” con su campo “SRC”. O como un “IFRAME”. O como una especificación de hoja de estilos mediante un “LINK” con su correspondiente “HREF”. Por ejemplo:

<html>
<head><title>Prueba</title></head>
<body>
<h1>Colores</h1>
<table border="1">
<tr><td>Rojo</td><td><img src="rojo.png"></td></tr>
<tr><td>Azul</td><td><img src="azul.png"></td></tr>
<tr><td>Verde</td><td><img src="verde.png"></td></tr>
</table>
</body>
</html>


Para mostrar las imágenes “rojo.png”, “azul.png” y “verde.png”, nuestro navegador necesita procesar otros ficheros. Existe, sin embargo, un esquema, un tipo, de URI que permite indicar el contenido de los datos externos directamente dentro del documento HTML. Se trata de las URI “data”, de las que en su día traté en relación con cierta vulnerabilidad en
y en

Resumiendo, para insertar una imagen en formato JPEG, cuyo tipo MIME asociado es “image/jpeg”, se podría usar una URIs como:
data:image/jpeg;base64, /9j/4AAQSkZJRgABAQAAAQABAAD...

… donde lo que sigue a “base64,” es la representación en Base64 del contenido de la imagen. De este modo, podríamos escribir en un documento HTML algo del tipo:
<img src='data:image/jpeg;base64, /9j/4AAQSkZJRgABAQAAAQABAAD...'>

Pues, venga. Vamos a hacer un programa que nos convierta todas las referencias a ficheros externos en URIs de tipo “data”.


¿Qué necesito?

Para analizar los documentos HTML ya tengo mis clases. Pero aún no tengo nada para convertir ficheros en su representación en Base64.

No te preocupes. Ruby trae eso “de serie”. Y para utilizarlo, sólo hay que poner una cosita al principio de nuestro programa

require "base64.rb"

El fichero “base64.rb”, que Ruby sabrá dónde lo guarda, no contiene una clase, sino un “MÓDULO”. Un módulo es un conjunto de métodos y otras definiciones. Algo así como una librería.

Y uno de los métodos definidos en el módulo “Base64” hace exactamente lo que queremos: convertir una cadena a Base64. Se trata de Base64.strict_encode64:

irb(main):001:0> require 'base64.rb'
=> true
irb(main):002:0> a = Base64.strict_encode64('Hola. ¿Cómo estás?')
=> "SG9sYS4gwr9Dw7NtbyBlc3TDoXM/"
irb(main):003:0>

Fíjate en que para llamar a los métodos de un módulo hay que ponerles antes el nombre del módulo y un punto: Base64.strict_encode64

Creando nuestro propio módulo

Para que nos sirva de ejemplo, vamos a crearnos un módulo muy sencillito. El fichero que lo contiene se llamará “mime.rb” y contendrá métodos para determinar el tipo MIME de las cosas. Su contenido será:
module MimeCheck
   def extensionToMime(uri)
      extension = uri.split('.').last
      return case extension.downcase
         when 'htm' then 'text/html'
         when 'html' then 'text/html'
         when 'php' then 'text/html'
         when 'chm' then 'text/html'
         when 'asp' then 'text/html'
         when 'aspx' then 'text/html'
         when 'jpg' then 'image/jpg'
         when 'jpeg' then 'image/jpg'
         when 'png' then 'image/png'
         when 'gif' then 'image/gif'
         when 'pdf' then 'application/pdf'
         when 'js' then 'application/javascript'
         when 'swf' then 'application/shockwave-flash'
         when 'css' then 'text/css'
         else 'unknown/unknown'
      end
end
end # module MimeCheck

Como puede observarse, la definición del módulo comienza con la palabra “module”, seguida del nombre del módulo y termina, como casi siempre, con “end”

Nuestro módulo sólo tiene un método, “extensionToMime” que trata de adivinar el tipo MIME de un archivo a partir de su extensión, que se determina a partir de la ruta en la línea:
extension = uri.split('.').last

Donde “uri” es una cadena con la ruta. El método “split” divide esta cadena en trozos y crea un array con ellos. El parámetro pasado a “split”, '.', le indica que los trozos que queremos obtener están separados en la cadena original por el carácter punto (.).

De todos estos trozos, nos quedamos con el último con “last”. Y ya se sabe que lo que sigue al último punto en el nombre de un archivo es su extensión...

Eso es todo. Ahora, cuando deseemos usar la función “extensionToMime”, aparte de hacer un require del fichero “mime.rb”, tendremos que escribir “MimeCheck.extensionToMime”.

¿Te parece demasiado? Si escribir mucho te gusta poco o nada, te comprendo. Y parece que también te comprendió en su día el creador de Ruby, porque hay una instrucción muy apañada: “include”. Puedes poner en tu programa

include MimeCheck

… y a partir de ahí, usar las funciones del módulo MimeCheck directamente, como en:
mime = extensionToMime(file)


¡Hala! A escribir

Vamos ya con nuestro programa. El fichero que lo contiene se llamará “poc.rb”. Y comenzará con:
require "base64.rb"
require File.dirname(__FILE__) + '/htmlparser.rb'
require File.dirname(__FILE__) + '/mime.rb'
include MimeCheck

Hora de escribir el método que analizará y procesará los documentos HTML. Lo primero es decidir qué etiquetas y atributos vamos a modificar. Por ejemplo, los “HREF” de los “LINK”. O los “SRC” de las “IMG”. O los “SRC” de los “SCRIPT”. O...

Para que sea fácil modificarlo, vamos a crear una variable que apunte a un array. Los elementos de este array tendrán dos elementos: Un nombre de etiqueta y un nombre de atributo a procesar para esa etiqueta:
# Cada par [tag, attr] indica que se desea convertir a data:
# el atributo "attr" de las etiquetas "tag"


$targets = [ ['LINK', 'HREF'], ['SCRIPT','SRC'], ['IMG','SRC'], ['IFRAME','SRC'], ['OBJECT','DATA'] ]

¡Quieto Parao! ¿Qué ese dólar?

Sí. Hay un dólar al inicio del nombre de la variable “$targets”. Ese dólar quiere decir que la variable es global. Que está accesible para los métodos de nuestro programa. Por lo demás, podemos utilizarla como ya venimos haciendo con otros tipos de variables: se le asigna valor y se utiliza después

Procesar el fichero HTML

El método que procesa el fichero HTML es “dataHTML”:
def dataHTML(file, dirbase = '')
   if not File.readable?(file)
      return ''
   end

   # Si no se proporciona directorio base, tomar el del fichero a procesar
   base = (dirbase == '' ? File.dirname(File.absolute_path(file)) : dirbase)

   # Valor a retornar
   res = ''

   # Convertir el fichero en representación de HTML
   # Y modificar las etiquetas y atributos indicados en $targets
   HTMLParser.new(File.read(file)).parseHTML.each do |element|
      if element.class.name == 'HTMLTag'
         $targets.select{|t| element.name == t[0] and element.attributes.has_key?(t[1])}.collect{|s| s[1]}.each do |attribute|

            # Determinar fichero a convertir a data:
            attrfile = element.attributes[attribute]
            if attrfile != ''
               if attrfile[0] != '/' and not(attrfile.match(/^[A-Za-z]:\\/))
                  attrfile = base + '/' + attrfile
               end

              # Convertir el fichero en data:
               element.attributes[attribute] = data(attrfile, base)
            end
         end
      end

      # Añadir a la salida la reperesentación del elemento
      res += element.to_s
   end

   return res
end

Al principio, realizamos alguna comprobación sobre el fichero que nos pasan como argumento. Con “File.readable?” determinamos si es posible leer el fichero. Con “File.absolute_path” convertimos una ruta de archivo relativa en una ruta absoluta. Poco más.

El meollo está en
HTMLParser.new(File.read(file)).parseHTML.each do |element|
   if element.class.name == 'HTMLTag'
      $targets.select{|t| element.name == t[0] and element.attributes.has_key?(t[1])}.collect{|s| s[1]}.each do |attribute|

La primera línea abre el fichero para lectura (r) en modo binario (b) y lee todo su contenido (File.read(file)) y se lo pasa a un objeto de tipo HTMLParser para que lo analice con “parseHTML”. Eso nos retornará un array de los elementos que componen el documento.

De ellos, sólo nos interesan los que sean de la clase HTMLTag. Las etiquetas. Los demás no los vamos a tocar.

Y en la tercera línea...¡Vaya, qué larga! ¡Posiblemente te aparezca partida en dos...! Para cada etiqueta de nuestro documento, cogemos el array “$targets” y seleccionamos de él aquellas parejas cuyo primer elemento (el nombre de etiqueta) sea igual al nombre de nuestra etiqueta y cuyo segundo elemento (el nombre del atributo) esté entre las claves del hash de atributos de nuestro elemento. Y con “collect” nos quedamos con el nombre del atributo (el segundo elemento de la parejita).

De ese modo, tenemos una lista de los atributos de nuestra etiqueta que tenemos que procesar. Le ponemos el “each” de turno y el bloque de código de qué hay que hacer con ellos:

# Determinar fichero a convertir a data:
attrfile = element.attributes[attribute]
if attrfile != ''
   if attrfile[0] != '/' and not(attrfile.match(/^[A-Za-z]:\\/))
      attrfile = base + '/' + attrfile
   end

   # Convertir el fichero en data:
   element.attributes[attribute] = data(attrfile, base)
end

El contenido de dicho atributo debe ser una ruta de fichero. Si no es una cadena vacía, comprobamos si la ruta es relativa y, de serlo, la pasamos a absoluta poniéndole delante el directorio base. Observa que en la expresión regular:
/^[A-Za-z]:\\/
…, que trata de bucar cosas como “c:\” o “D:\”, hay dos cosas reseñables:
  • Que al principio hay un carácter “^”. Cuando nos encontramos con este carácter fuera de una categoría (fuera de corchetes) su significado es distinto del que hemos visto en otras ocasiones: En este caso significa “al principio de la cadena o al principio de una línea”.
  • Que para indicar una barra invertida, “\”, ponemos dos “\\”. Recuerda que la barra invertida le quita los significados especiales a los caracteres que lo tienen. Es como la Kryptonita para Superman. Así que, para que una barra invertida pierda su poder de neutralizar lo que sigue, le ponemos delante... otra barra invertida.

Y, directamente, convertimos el contenido del atributo en una URI de tipo “data” llamando a la función “data”:

def data(file, dirbase)
   # Determinar el tipo del fichero
   mime = extensionToMime(file)

   x = 'data:' + mime + ';base64,'

   if mime == 'text/html'
      return x + Base64.strict_encode64(dataHTML(file))
   else
      return x + (File.readable?(file) ?
                       Base64.strict_encode64(open(file,"rb").read) : '')
   end
end

Relativamente sencilla. Primero se determina el tipo MIME del fichero. Si el documento es de tipo HTML, llama de forma recursiva a dataHTML y convierte el resultado obtenido en Base64 y se crea con lo que obtenemos una URI de tipo data.

En caso contrario, se codifica en Base64 el contenido y se crea la URI data.

Pues ya está casi todo.

Haciéndolo andar

Bueno, ya sólo falta que el programa sepa qué fichero contiene el documento HTML que hay que convertir y dónde tiene que guardar los resultados. Para ello se utilizarán parámetros de línea de comandos e invocaremos nuestro programa así:

ruby poc.rb fichero_de_entrada fichero_de_salida

El programa puede acceder a los parámetros de línea de comandos mediante una variable de tipo Array predefinida llamada “ARGV”. Así,
ARGV[0]
.. será el primer parámetro,
ARGV[1]
… será el segundo. Y así sucesivamente.

Lo primero es comprobar si nos han pasado dos parámetros:
if ARGV.length != 2
   print "Uso: #{$0} fichero_entrada fichero_salida\n\n"
else

Observa esa cadena, que es rara de coj@#!!&% Y es que dentro de una cadena encerrada entre comillas dobles podemos insertar el resultado de evaluar una expresión. Para eso se pone una almohadilla “#” seguida de la expresión a evaluar encerrada entre llaves (“{“ y “}”).

En este caso, la expresión a evaluar es “$0”. Ésta es una variable global predefinida que nos proporciona el nombre del fichero que contiene el programa.

En definitiva, si no le pasamos dos argumentos, nuestra fiera se queja:
$ ruby poc.rb
Uso: poc.rb fichero_entrada fichero_salida
$

Ya sólo queda, identificar los ficheros que tenemos que utilizar:
# Determinar ficheros a utilizar
input_file = ARGV[0]


if not File.readable?(input_file)
   raise "No se puede leer #{input_file}\n"
end

output_file = ARGV[1]

… y procesar el fichero de entrada y guardar el resultado en el fichero de salida:
output = open(output_file, "wb")
output.write(dataHTML(input_file))
output.close

El fichero de salida se abre con open para escritura (w) en modo binario (b). Y se escribe su contenido (el resultado de procesar el fichero de entrada) con “write”. Finalmente, se cierra el fichero con “close”.

Y ya está.

El fichero poc.rb quedará...


require "base64.rb"
require File.dirname(__FILE__) + '/htmlparser.rb'
require File.dirname(__FILE__) + '/mime.rb'
include MimeCheck

# Cada par [tag, attr] indica que se desea convertir a data:
# el atributo "attr" de las etiquetas "tag"
$targets = [ ['LINK', 'HREF'], ['SCRIPT','SRC'], ['IMG','SRC'], ['IFRAME','SRC'], ['OBJECT','DATA'] ]

# Convierte elementos externos de un fichero HTML en elementos data:
# Parámetros
# file: fichero con código HTML
# dirbase: directorio a utilizar como base para rutas relativas
def dataHTML(file, dirbase = '')
   if not File.readable?(file)
      return ''
   end

   # Si no se proporciona directorio base, tomar el del fichero a procesar
   base = (dirbase == '' ? File.dirname(File.absolute_path(file)) : dirbase)

   # Valor a retornar
   res = ''

   # Convertir el fichero en representación de HTML
   # Y modificar las etiquetas y atributos indicados en $targets
   HTMLParser.new(open(file,"rb").read).parseHTML.each do |element|
      if element.class.name == 'HTMLTag'
         $targets.select{|t| element.name == t[0] and element.attributes.has_key?(t[1])}.collect{|s| s[1]}.each do |attribute|

            # Determinar fichero a convertir a data:
            attrfile = element.attributes[attribute]
            if attrfile != ''
               if attrfile[0] != '/' and not(attrfile.match(/^[A-Za-z]:\\/))
                  attrfile = base + '/' + attrfile
               end

               # Convertir el fichero en data:
               element.attributes[attribute] = data(attrfile, base)
            end
         end
      end

      # Añadir a la salida la reperesentación del elemento
      res += element.to_s
   end

   return res
end


# Convierte un fichero en data:
# Parámetros igual que en dataHTML
def data(file, dirbase)
   # Determinar el tipo del fichero
   mime = extensionToMime(file)

   x = 'data:' + mime + ';base64,'

   if mime == 'text/html'
      return x + Base64.strict_encode64(dataHTML(file))
   else
      return x + (File.readable?(file) ?
            Base64.strict_encode64(open(file,"rb").read) : '')
   end
end


# Comprobar que se han pasado dos parámetros por línea de comandos
if ARGV.length != 2
   print "Uso: #{$0} fichero_entrada fichero_salida\n\n"
else

   # Determinar ficheros a utilizar
   input_file = ARGV[0]
   if not File.readable?(input_file)
      raise "No se puede leer #{input_file}\n"
   end

   output_file = ARGV[1]

   # Realizar los cambios y guardar los resultados
   output = open(output_file, "wb")
   output.write(dataHTML(input_file))
   output.close

end

Y a usarlo

¿Recuerdas el fichero HTML que pusimos de ejemplo al principio? No es que sea demasiado impresionante, pero, en conjunto, quedaría algo así:


El fichero se llama "1.html" y está en el directorio "pagina". Vamos a convertirlo con nuestro programita:
$ ruby poc.rb pagina/1.htm salida.htm

En unos instantes habrá acabado y podremos abrir el fichero de salida



Sí. Tiene la misma pinta que el original. Pero si lo miramos por dentro, comprobaremos que las imágenes están puestas como URIs de tipo data.


¡Tachaaaaaaaaán!