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.