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
  

 

No hay comentarios:

Publicar un comentario