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 “"e;”. 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(/"/, '"e;') |
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(/"/, '"e;') => "a"e;b"e;c" irb(main):015:0> a # Comprobemos que el valor de a no ha cambiado => "a\"b\"c" irb(main):016:0> a.gsub!(/"/, '"e;') => "a"e;b"e;c" irb(main):017:0> a # Con gsub! Sí cambia => "a"e;b"e;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(/"/,'"e;') + '"'} |
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(/"/,'"e;') + '"'}.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(/"/,'"e;') + '"'}.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(/"/,'"e;') + '"'}.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(/"/,'"e;') + '"'}.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