martes, 26 de febrero de 2013

Representar cosas de HTML

Ya es hora de hacer algo útil

Bueno, cuatro capítulos dando rodeos es, creo, suficiente. Vamos ya con las primeras clases “interesantes” para nuestro analizador de código HTML: aquellas que representan los distintos elementos que podemos encontrar en este tipo de documentos.

A los efectos que nos ocupan, es conveniente distinguir cinco tipo de elementos:
  • Las etiquetas de apertura, como “<b>”, “<hr />” o “<a href='http://418iamateapot.blogspot.com'>”. 
    En ellas se pueden reconocer tres elementos fundamentales: el nombre, la opcional lista de atributos y, a veces, el carácter “/” antes del “>” para indicar que la etiqueta no va a tener un cierre más adelante.
  • Las etiquetas de cierre como “</a>” que indican dónde acaban los “efectos” de una etiqueta abierta anteriormente.
  • Los textos independientes
  • Los comentarios, como “<!-- Esto es un comentario -->”
  • Las definiciones, construcciones incorrectas y “otras cosas” que HTML admite sin que sean parte del lenguaje.

Así que abre tu editor de textos y crea un fichero llamado “htmlthings.rb” en la misma carpeta en la que guardaste anteriormente “attributes.rb” y “nocase.rb”.


Eso de las rutas

Vamos a ver, paso por paso, qué hay que escribir para que “htmlthings.rb” sirva para algo. Lo primero es incluir las clases Attributes y Nocase que ya tenemos definidas. Algo que, como ya vimos, se puede conseguir con unas líneas como las siguientes:
require './attributes.rb'
require './nocase.rb'

¡Pero no escribas nada todavía! Piensa un poco. ¿Qué significa eso de './attributes.rb'? El fichero se busca en el directorio actual. Bueno, eso funciona... siempre que nuestro directorio actual sea el mismo en el que están las cosas. Pero... ¿y si el usuario ni siquiera sabe dónde se alojan los scripts? ¿y si los puede ejecutar porque se encuentran en uno de los directorios donde su sistema operativo busca los programas (eso que llaman "el PATH")?

Lo que necesitamos es alguna forma de determinar en tiempo de ejecución la ruta en que se encuentran los scripts de Ruby. Y hay forma de hacerlo. Para empezar, con __FILE__ se obtiene una String con la ruta del script que se está ejecutando.

Ahora sólo hace falta determinar el directorio correspondiente a dicha ruta. Y la clase File, que está disponible “de fábrica” tiene un método llamado dirname que hace eso. Así que, uniendo ambos queda:

require File.dirname(__FILE__) + '/attributes.rb'
require File.dirname(__FILE__) + '/nocase.rb'

Ahora sí. Copia estas dos líneas a tu fichero.

Varias clases que no hacen nada

Y ahora comencemos a especificar cómo funcionan nuestros objetos. Para tres de los tipos enumerados al principio, textos, comentarios y lo que antes denominamos “otras cosas”, no vamos a necesitar demasiados detalles. Por eso, sus correspondientes clases se especificarán como subclases de String.

No hay problema en que juntemos más de una clase en un mismo fichero. Empecemos con estas tres:

# HTML Text, comments and other things / Textos, comentarios y otras cosas de HTML


class HTMLText < String
end


class HTMLComment < String
end


class HTMLOther < String
end

Copia. Copia sin miedo. Ya después, si es que nos hace falta, podremos cambiarlo.


La clase de la etiqueta de cierre

Para las etiquetas de cierre, sólo nos interesa saber el nombre. Añade esta clase
# HTML Closing Tag / Etiqueta de Cierre HTML
class HTMLClose

# Closing Tag Data / Datos de la etiqueta de cierre
    attr_reader :name


# Object creation / Creación de objetos
    def initialize(name)
        @name = Nocase.new(name)
    end


# Convert to string / Convertir a cadena
    def to_s
        '</' + @name.downcase + '>'
    end
end

¿Has terminado ya de copiarlo? Pues expliquemos unas cuantas cosas.

Ya hemos visto algunos ejemplos de uso de variables y asignaciones de valores en los capítulos anteriores. Verdad que pasamos de puntillas, como quien no hace la cosa, pero es que es algo que, en principio, no precisa demasiadas explicaciones. Pero, por si acaso...

Variables de instancia

Una variable en Ruby es una referencia a un objeto. Una forma de acceder a él. Una referencia a la zona de memoria en que el objeto está ubicado.

O, en otras palabras, la variable NO CONTIENE EL VALOR O LOS VALORES DEL OBJETO, sino que nos dice “dónde vive”.

Y eso tiene sus repercusiones. Por ejemplo, si a una variable le asignamos otra, ambas referenciarán al mismo objeto. Y cualquier cambio que se haga a una afectará a la otra. En el próximo ejemplo (no vayas a copiarlo en el fichero, que es para que experimentes con irb), a y b señalan al mismo objeto:

irb(main):001:0> a = '1234'
=> "1234"
irb(main):002:0> b = a
=> "1234"
irb(main):003:0> a.insert(0,'abcd')
=> "abcd1234"
irb(main):004:0> a
=> "abcd1234"
irb(main):005:0> b
=> "abcd1234"

Observa cómo se define el método initialize (no lo copies, que ya lo hiciste antes ¡ayyy, que cruz!):
def initialize(name)
    @name = Nocase.new(name.strip)
end

Eso de @name es... una variable especial. Cuando una variable comienza por una arroba “@”, eso quiere decir que cada instancia de la clase, cada objeto que creemos, tendrá asociada una variable con dicho nombre.

En este caso, cada objeto de la clase HTMLClose tendrá una variable de instancia llamada @name. A efectos prácticos, la variable se crea en el momento en que se le asigna un valor por primera vez. Y lo que usamos como nombre es una de esas cadenas que creamos en el capítulo anterior y que no distinguen entre mayúsculas y minúsculas.

En cuanto a “strip”... nada que ver con eso que estás pensando. Aplicado a una String, strip retorna el resultado de eliminar de aquella todos los espacios iniciales y finales que pudiera tener.

De alguna forma habrá que poder acceder

En principio, la variable @name no estará accesible más que para el propio objeto al que pertenece. Si queremos que pueda referenciarse desde fuera, formas hay de hacerlo. Observa esto que ya copiaste al fichero “htmlthings.rb”

attr_reader :name

Con eso se indica que, desde fuera, se puede acceder a @name sólo para lectura. Sólo para consultar su valor.

Si se quisiera poder modificar su valor, pero no leerlo, en lugar de attr_reader se utilizaría attr_writer. Y si se deseara poder leer y modificar pondríamos attr_accessor.

En los tres casos, se puede indicar más de un atributo, separándolos con comas.

Por si necesitamos convertirlo en cadena...

La clase se completa con la definición del método to_s, que se suele usar para realizar la conversión a cadena
def to_s
    '</' + @name.downcase + '>'
end


Y otra clase para las etiquetas

La clase HTMLTags definirá una etiqueta de HTML, con su nombre, sus atributos y todo lo demás. Vamos a ir viendo poco a poco sus métodos y lo ponemos todo junto al final.

Como variables de instancia vamos a tener el nombre, el hash de atributos y un indicador de si la etiqueta termina en “/>” y, por tanto, se cierra ella misma solita.

Por un lado, el nombre no debe distinguir entre mayúsculas y minúsculas. Y, por otro, tampoco los nombres de los atributos, que para eso creamos antes la clase Attributes. Con todo ello en mente, podemos empezar por:
# Access to Tag Data / Acceso a los datos de la Etiqueta
attr_reader :name, :autoclosed, :attributes


# Object Creation / Creación de objetos
def initialize(name, autoclosed=false, attributes={})
    @name = Nocase.new(name.strip)
    @autoclosed = autoclosed
    @attributes = Attributes.new(attributes)
end

Ahora vamos a ver cómo convertir en una cadena un objeto de la clase HTMLTag. Para empezar, habrá que poner un “<” seguido del nombre de la etiqueta. Y, a continuación, los atributos con sus valores.

La lista de atributos es un hash en el que los índices son los nombres de sus atributos y sus correspodientes items sus valores. Para cada par “indice => valor” del hash, habría que poner un espacio separador, el índice, un signo igual “=” y el valor entre comillas. Así si tenemos un elemento como
'href' => 'http://418iamateapot.blogspot.com'

… nos quedaría
href="http://418iamateapot.blogspot.com"

Además, si el valor del atributo contuviera alguna comilla doble ("), ésta debe ser sustituida por “&quote;”. Bien ya sabemos qué queremos hacer.

Ahora, a ver cómo se consigue.

Reemplazando en cadenas

Lo de reemplazar una cadena por otra es cosa sencilla. String tiene un método llamado “gsub” que sustituye una cosa, todas las veces que aparezca, por otra. Más o menos así:
cadena.gsub(/"/, '&quote;')

Lo de /"/ te parecerá una forma rara de poner una cadena. Tan raro como que no lo es. En realidad se trata de una cosa muy interesante llamada “expresión regular”. Pero, por ahora, conténtate con saber que /"/ representa una comilla doble. En las próximas entregas tendremos expresiones regulares para hartarnos.

Existe otro método, “sub”, que hace lo mismo que “gsub” pero sólo sustituye las cosas la primera vez que las encuentra.

Otro detalle: ni “sub” ni “gsub” modifican la cadena a la que se aplican. Si quisiérmaos alterarlas existen otros métodos que sí lo hacen. Prueba esto en irb:
irb(main):013:0* a = 'a"b"c'
=> "a\"b\"c"
irb(main):014:0> a.gsub(/"/, '&quote;')
=> "a&quote;b&quote;c"
irb(main):015:0> a # Comprobemos que el valor de a no ha cambiado
=> "a\"b\"c"
irb(main):016:0> a.gsub!(/"/, '&quote;')
=> "a&quote;b&quote;c"
irb(main):017:0> a # Con gsub! Sí cambia
=> "a&quote;b&quote;c"

Eso de poner una exclamación “!” al final de las nombres de las versiones de métodos que modifican el valor del objeto es algo habitual. No es obligatorio, pero la gente suele hacerlo.

Procesar todo un hash de una vez

En cuanto a lo de ir recorriendo uno a uno los elementos del hash de atributos, generando la cadena correspondiente y todo lo demás, Ruby permite una notación bastante compacta gracias a uno de los métodos de la clase Hash, denominado “collect”.

collect recorre todos los elementos del hash, uno a uno. Y, para cada uno, genera un valor (tendremos que decirle cómo, claro). Finalmente, con todos esos valores que ha generado, forma un array y nos lo retorna.

Queda por saber cómo se le indica la forma de calcular los valores a partir de cada elemento. Veamos un ejemplo:

@attributes.collect {|x,y| '  ' + x + '="' + y.gsub(/"/,'&quote;') + '"'}

Lo que se pone a continuación de “collect” es un bloque de código. Ya vimos otra notación para los bloques de código antes. Algo como:
do |x,y|
instrucciones
end

Pues bien, eso es equivalente, más o menos, a
{|x,y| instrucciones }

El bloque de código que se le pasa a “collect” tiene dos “parámetros”. El primero es el índice del item y el segundo su valor asociado. Con ellos, el bloque genera el correspondiente texto siguiendo las directrices que nos marcamos antes.

Ya tenemos un array con las cadenas correspondientes a cada atributo. Ahora necesitaríamos unirlas. Y de nuevo hay un método, en este caso de la clase Array, que sirve precisamente para eso. Se llama join
irb(main):018:0> ['a', 'b', 'c'].join
=> "abc"

Con ello, la expresión para pasar los atributos a cadena quedaría
@attributes.collect {|x,y| ' ' + x + '="' + y.gsub(/"/,'&quote;') + '"'}.join

Después de los atributos viene el cierre de la etiqueta, que vendrá dado por un carácter “>” o, si la etiqueta se cierra ella misma, la secuencia “/>”. En definitiva, la barra “/” se pondrá sí y solo sí la etiqueta se “autocierra”.

Para que el valor de una expresión dependa de una expresión se puede usar un operador quizá conocido para quienes conozcan otros lenguajes de programación:
condicion ? valor_si_cierto : valor_si_falso

O, en nuestro caso, para que se ponga la barra si @autoclosed es cierto y nada si no lo es:
@autoclosed ? '/' : ''

Pongámoslo ahora todo junto y tendremos:
def to_s
    '<' + @name.downcase + @attributes.collect {|x,y| ' ' + x + '="' + y.gsub(/"/,'&quote;') + '"'}.join +
         (@autoclosed ? '/' : '') + '>'

end





En definitiva, la clase HTMLTag quedará:

Esto es lo que tienes que copiar

class HTMLTag
    # Access to Tag Data / Acceso a los datos de la Etiqueta
    attr_reader :name, :autoclosed, :attributes


     # Object Creation / Creación de objetos
    def initialize(name, autoclosed=false, attributes={})
        @name = Nocase.new(name.strip)
        @autoclosed = autoclosed
        @attributes = Attributes.new(attributes)
    end


    # Convert Tag to string / Convertir la etiqueta en cadena
    def to_s
       '<' + @name.downcase + @attributes.collect {|x,y| ' ' + x + '="' + y.sub(/"/,'&quote;') + '"'}.join
        + (@autoclosed ? '/' : '') + '>'
    end
end


Pongámoslo todo junto



El fichero “htmlthings.rb” quedará finalmente como sigue:
.
require File.dirname(__FILE__) + '/attributes.rb'
require File.dirname(__FILE__) + '/nocase.rb'


# HTML Tag / Etiqueta HTML
class HTMLTag
    # Access to Tag Data / Acceso a los datos de la Etiqueta
    attr_reader :name, :autoclosed, :attributes

    # Object Creation / Creación de objetos
    def initialize(name, autoclosed=false, attributes={})
        @name = Nocase.new(name.strip)
        @autoclosed = autoclosed
        @attributes = Attributes.new(attributes)
    end

    # Convert Tag to string / Convertir la etiqueta en cadena
    def to_s
        '<' + @name.downcase + @attributes.collect {|x,y| ' ' + x + '="' + y.sub(/"/,'&quote;') + '"'}.join
        + (@autoclosed ? '/' : '') + '>'
    end
end


# HTML Closing Tag / Etiqueta de Cierre HTML
class HTMLClose
    # Closing Tag Data / Datos de la etiqueta de cierre
    attr_reader :name

    # Object creation / Creación de objetos
    def initialize(name)
        @name = Nocase.new(name)
    end


    # Convert to string / Convertir a cadena
    def to_s
        '</' + @name.downcase + '>'
    end
end


# HTML Text, comments and other things / Textos, comentarios y otras cosas de HTML


class HTMLText < String
end

class HTMLComment < String
end


class HTMLOther < String
end



No hay comentarios:

Publicar un comentario