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!

viernes, 1 de marzo de 2013

El analizador (¡ya era hora!)



¡La última entrega! Si la cosa ha sido pesada, lo siento. Pero espero que te haya servido de algo.

Recapitulando, la última vez que nos vimos estábamos editando un fichero llamado “htmlparser.rb” y habíamos puesto en él unos cuantos “require” y la definición de la clase AttrParser. Hoy vamos a seguir con él y le añadiremos una nueva clase.

¡Y con eso habremos acabado!

Por ahora, claro.

Nuestra clase para analizar código HTML (¡al fin!) se va a llamar HTMLParser y será una subclase de Tokenizer:
class HTMLParser < Tokenizer
end

¡Hala! Añáde eso al final de “htmlparser.rb”. Y vamos a rellenar la clase...

Creando el objeto

La creación del objeto HTMLParser seguirá una estructura muy similar a la que vimos para AttrParser en el post anterior:
def initialize(html='')
            super(html)
                       
            @attrparser = AttrParser.new
                       
            comment = '<!--.*?-->'
            spaces = '[[:space:]]+'
            string1 = "'[^']*'"
            string2 = '"[^"]*"'
            nostring = "[^'\"[[:space:]]>=][^>[[:space:]]=]*"
                       
            attrs = '(?<attrs>' +
                                   '(' + spaces + '|' + string1 + '|' + string2 + '|' + nostring + '|=)*' +
                        ')'
                                  
            name = '(?<name>[a-z][^[[:space:]]>]*)' # Ignorecase está activado en Tokenizer

            # Reglas para reconocer cosas
            addRule(comment,:comment)

            addRule(/<!>/,:other)
            addRule(/<![^->].*?>/,:other)
            addRule(/<!-[^->].*?>/,:other)
            addRule('<!' + name + attrs + '>', :other)
                       
            addRule('<' + name + attrs + '>', :element)
            addRule('</' + name + attrs + '>', :close)

            addRule(/./,:text)
end

Poco nuevo que reseñar. Creamos un objeto de clase AttrParser para más adelante usarlo para analizar los atributos de las etiquetas. Después comenzamos a definir los patrones para las expresiones regulares. La primera es la de los comentarios:

            comment = '<!--.*?-->'

Aquí el interrogante tiene un significado especial. ¿Recuerdas que te dije que cuando Ruby se encuentra un carácter de repetición, como * o +, siempre se queda con la cadena más larga posible (eso del “burro grande, ande o no ande”). Pues hay veces que eso no nos interesa y ésta es una de ellas.

Queremos que, una vez iniciado el comentario con “<!--”, éste acabe en cuanto se encuentre una secuencia de final de comentario, “-->”. En medio puede haber cualquier cosa. O sea, en lenguaje de las expresiones regulares, “.*” .

Pero ¿qué pasa si hay dos comentarios? Por ejemplo, como en:
<!-- Comentario 1 -->
<a href='#abcd'>Haz clic aqu&iacute;</a>
<!-- Comentario 2 -->

Si ponemos la expresión regular
<!--.*-->

… y la comparamos con el texto anterior, tendremos dos posibles coincidencias:
         El comentario de la primera línea
         Todo el texto

Y, con la regla del burro grande, Ruby se quedará con la segunda. Que no es lo que nosotros queremos. Para que el asterisco no sea tan glotón y se conforme con lo primero que se encuentre, con la coincidencia más corta, le podemos poner detrás el carácter de cierre de interrogación (?):
<!--.*?-->

Otra construcción nueva de expresiones regulares que encontramos es la barra (|). Mediante ella podemos indicar varias opciones posibles.

Pongamos un ejemplo. Si queremos reconocer una cualquiera de las palabras “cuento”, “clase” y “casa”, podríamos usar la expresión regular:
/cuento|clase|casa/

Tendríamos otras formas de hacerlo. Dado que las tres palabras comienzan por la letra “c”, podríamos poner:
/c(uento|lase|asa)/

Observa como se usan los paréntesis para agrupar las expresiones regulares. Si quieres, puedes irte al irb y hacer pruebas:

irb(main):052:0> /c(uento|lase|asa)/.match('Esto es un cuento chino')
=> #<MatchData "cuento" 1:"uento">
irb(main):053:0> /c(uento|lase|asa)/.match('Esto es una clase de Ruby')
=> #<MatchData "clase" 1:"lase">
irb(main):054:0> /c(uento|lase|asa)/.match('Esto es una casa de ladrillo ')
=> #<MatchData "casa" 1:"asa">

Eso es lo más relevante. Fíjate, eso sí, en que, para que nuestro analizador se trague incluso el HTML mal creado, permitimos cosas raras como, por ejemplo, que las etiquetas de cierre contengan definiciones de atributos. Tu navegador posiblemente también lo haga...


Otra cosa es que la regla para los nodos de texto es muy corta:
addRule(/./,:text)

La idea es que si nos encontramos con algo que no puede ser reconocido como :element, ni como :close, ni como :comment, ni como :other, descartamos el primer carácter (que consideraremos como texto) y volvemos a probar.


Analizar el código

Eso nos obligará después a combinar todos los caracteres de texto consecutivos en un único nodo de tipo :text. Eso será en el método que extrae los distintos elementos del documento HTML y los procesa:
def parseHTML(html=nil)
            if html != nil
                        loadStr(html)
            end
                       
            # Array a retornar
            result = []
                       
            # text es usado para llevar la cuenta de
            # los nodos de texto consecutivos
            text = ''
                       
            getTokens.each do |x|
                        if x.class.name == "HTMLText"
                                   # Unir nodos de texto consecutivos en uno sólo
                                   text += x.to_s
                        else
                                   # Si hay texto pendiente, añadirlo
                                   if text != ''
                                               result.push(HTMLText.new(text))
                                               text = ''
                                   end
                                   # Añadir el nodo
                                   result.push(x)                                                            
                        end
            end
                       
            # Si hay texto pendiente, añadirlo
            if text != ''
                        result.push(HTMLText.new(text))
            end
                       
            return result
end


Las cosas del HTML

Y ya sólo nos quedarían los métodos privados que se encargan de crear los objetos que representan elementos encontrados. El más relevante es el correspondiente a las etiquetas:
def element(x)
            autoclosed = (x[:attrs][-1] == '/')
                       
            # Texto de los atributos (quitar el / del final si lo hay)
            attrs = autoclosed ? x[:attrs][0..-2] : x[:attrs]
            attrs.strip!
                       
            return HTMLTag.new(x[:name], autoclosed, @attrparser.parseAttr(attrs))
end

Lo primero que se comprueba es si el último carácter del texto extraído para los atributos  es una barra inclinada /.  Si es así, la etiqueta estará auto-cerrada y debemos eliminar el último carácter antes de analizar los atributos.

Fíjate en eso de “x[:attrs][-1]”. x[:attrs] es una cadena. Y [-1] es la forma de extraer su último carácter. Me expico: Para extraer los caracteres de una cadena se utilizan índices, como en los arrays. El cero se corresponde con el primer carácter:
irb(main):060:0> 'prueba'[0]
=> "p"
irb(main):061:0> 'prueba'[1]
=> "r"

Si lo que queremos es seleccionar un elemento contando a partir del final de la cadena hacia atrás, se usan números negativos. -1 se correspondería con el último, -2 con el penúltimo, etc.
irb(main):062:0> 'prueba'[-1]
=> "a"
irb(main):063:0> 'prueba'[-2]
=> "b"

Y, si queremos un rango de caracteres, también lo podemos indicar usando dos puntos seguidos para separar la primera y la última posición deseada. Podemos mezclar posiciones “positivas” y “negativas”:
irb(main):067:0> 'prueba'[2..4]
=> "ueb"
irb(main):068:0> 'prueba'[0..-2]
=> "prueb"
irb(main):069:0> 'prueba'[-3..-2]
=> "eb"

Al final, se crea y retorna un objeto de clase HTMLTag, al que se le pasa el nombre de la etiqueta, su condición de auto-cerrada o no y los atributos, debidamente analizados.

El resto de los tipos de “cosas HTML” tienen un tratamiento mucho menos glamouroso:
           
def close(x)
            return HTMLClose.new(x[:name])
end

def comment(x)
            return HTMLComment.new(x.to_s)
end
           
def other(x)
            return HTMLOther.new(x.to_s)
end
           
def text(x)
            return HTMLText.new(x.to_s)
end


… Y eso es todo.

La clase quedaría...

# Analizador de Documentos HTML
class HTMLParser < Tokenizer
            def initialize(html='')
                        super(html)
                       
                        @attrparser = AttrParser.new
                       
                        comment = '<!--.*?-->'
                       
                        spaces = '[[:space:]]+'
                        string1 = "'[^']*'"
                        string2 = '"[^"]*"'
                        nostring = "[^'\"[[:space:]]>=][^>[[:space:]]=]*"
                       
                        attrs = '(?<attrs>' +
                                               '(' + spaces + '|' + string1 + '|' + string2 + '|' + nostring + '|=)*' +
                                   ')'
                                  
                        name = '(?<name>[a-z][^[[:space:]]>]*)' # Ignorecase está activado

                        # Reglas para reconocer cosas
                        addRule(comment,:comment)

                        addRule(/<!>/,:other)
                        addRule(/<![^->].*?>/,:other)
                        addRule(/<!-[^->].*?>/,:other)
                        addRule('<!' + name + attrs + '>', :other)
                       
                        addRule('<' + name + attrs + '>', :element)
                        addRule('</' + name + attrs + '>', :close)

                        addRule(/./,:text)
            end

            # Analizar una cadena con código HTML
            # Parámetro: html -> cadena con el código a analizar
            # Retorna: Un array de elementos HTML
            def parseHTML(html=nil)
                        if html != nil
                                   loadStr(html)
                        end
                       
                        # Array a retornar
                        result = []
                       
                        # text es usado para llevar la cuenta de
                        # los nodos de texto consecutivos
                        text = ''
                       
                        getTokens.each do |x|
                                   if x.class.name == "HTMLText"
                                               # Unir nodos de texto consecutivos en uno sólo
                                               text += x.to_s
                                   else
                                               # Si hay texto pendiente, añadirlo
                                               if text != ''
                                                           result.push(HTMLText.new(text))
                                                           text = ''
                                               end
                                               # Añadir el nodo
                                               result.push(x)                                                            
                                   end
                        end
                       
                        # Si hay texto pendiente, añadirlo
                        if text != ''
                                   result.push(HTMLText.new(text))
                        end
                       
                        return result
            end
private
            # Methods for rules post-processing
            def element(x)
                        autoclosed = (x[:attrs][-1] == '/')
                       
                        # Texto de los atributos (quitar el / del final si lo hay)
                        attrs = autoclosed ? x[:attrs][0..-2] : x[:attrs]
                        attrs.strip!
                       
                        return HTMLTag.new(x[:name], autoclosed, @attrparser.parseAttr(attrs))
            end
           
            def close(x)
                        return HTMLClose.new(x[:name])
            end

            def comment(x)
                        return HTMLComment.new(x.to_s)
            end
           
            def other(x)
                        return HTMLOther.new(x.to_s)
            end
           
            def text(x)
                        return HTMLText.new(x.to_s)
            end
end # class HTMLParser



¡Gracias por llegar hasta aquí!

Nos vemos pronto en el próximo proyecto.