Triángulo de Sierpinski en JavaScript

Un amigo me propuso esta mañana que viera un vídeo de Numberphile, concretamente uno titulado Chaos Game. El vídeo es bastante interesante y habla de como de una aparente aleatoriedad es posible sacar fractales y patrones regulares. Esta misma mañana al llegar a casa y antes de comer me he picado y me he puesto a implementar el algoritmo en JavaScript usando el Canvas de HTML5. El resultado lo tenéis aquí:

http://adrianistan.eu/sierpinski/

Y el código que lleva es el siguiente:

const COLOR_LIST = ["red","green","yellow","pink","brown","purple","cyan","blue","orange"];

function punto(x,y){
    var p = {
        x:x,
        y:y
    };
    return p;
}

function puntoMedio(p,q){
    var m = {
        x: Math.round((p.x+q.x)/2),
        y: Math.round((p.y+q.y)/2)
    };
    return m;
}

function getRandomColor(){
    return COLOR_LIST[Math.floor(COLOR_LIST.length * Math.random())];
}

function dibujarPunto(ctx,p,size){
    ctx.fillStyle = getRandomColor();
    ctx.fillRect(p.x,p.y,size,size);
}

function $(id){
    return document.getElementById(id);
}

function get(id){
    return Math.round(document.getElementById(id).value);
}

function main(){
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");

    var interval;

    $("start").addEventListener("click",function(){

        var size = get("size");
        var vertex = [punto(get("a-x"),get("a-y")),punto(get("b-x"),get("b-y")),punto(get("c-x"),get("c-y"))];

        let p = punto(get("s-x"),get("s-y"));

        dibujarPunto(ctx,p,size);

        interval = setInterval(function(){
            var q = vertex[Math.floor(3*Math.random())];
            p = puntoMedio(p,q);
            dibujarPunto(ctx,p,size);
        },get("speed"));
    });

    $("stop").addEventListener("click",function(){
        clearInterval(interval);
    });

    $("reset").addEventListener("click",function(){
        ctx.fillStyle = "white";
        ctx.fillRect(0,0,600,600);
    });
}

window.addEventListener("DOMContentLoaded",main);

Con distinto número de vértices existen otros fractales, también muy chulos. Incluso en el vídeo de Numberphile realizan un fractal con un gran parecido a una hoja de helecho, usando dos triángulos y una ligera modificación del algoritmo.

Un saludo y soñad con fractales.

loading...

Tutorial de Neon – Combina Node.js con Rust

Hoy en día muchas webs se diseñan con Node.js. Es una solución fantástica para respuestas rápidas pero numerosos benchmarks han demostrado que su rendimiento empeora en respuestas complejas. Estos mismos benchmarks recomiendan usar Java o .NET si preveemos que nuestra aplicación web va a generar respuestas complejas. Sin embargo renunciar a las ventajas de Node.js no es del agrado de muchos. Afortunadamente hay otra solución, usar Rust. Todo ello gracias a Neon.

Con Neon podemos generar módulos para Node.js que son escritos y compilados en Rust con las ventajas que supone desde un punto de vista de rendimiento y con la certeza de que en Rust la seguridad está garantizada.

Usando Neon puedes desarrollar tu aplicación en Node.js y si alguna parte tiene problemas de rendimiento sustituirla por su equivalente en Rust. Para el ejemplo voy a hacer un módulo de Markdown.

Instalando Neon

En primer lugar instalamos la herramienta de Neon desde npm.

npm install -g neon-cli

Una vez esté instalado podemos usar la herramienta de Neon para construir un esqueleto de módulo. Este esqueleto tendrá dos partes, un punto de entrada para Node.js y otro para Rust.

neon new PROYECTO

Hacemos un npm install como nos indica. Esto no solo obtendrá dependencias de Node.js sino que también se encargará de compilar el código nativo en Rust.

El código Node.js

Nuestro módulo contiene un archivo de Node.js que sirve de punto de entrada. Allí lo único que se debe hacer es cargar el módulo en Rust y hacer de pegamento. Puede ser algo tan simple como esto:

var addon = require("../native");

module.exports = addon; // se exportan todos los métodos del módulo nativo

Aunque si queremos añadir un tratamiento específico también es posible.

var addon = require("../native");

module.exports = {
    render: function(str){
        return addon.render(str);
    }
}

El código en Rust

Nos dirigimos ahora al archivo native/src/lib.rs. Ahí definimos los métodos nativos que va a tener el módulo. Lo hacemos a través de la macro register_module!.

register_module!(m,{
    m.export("render",render)
});

Ahora vamos a implementar la función render, que toma el texto en Markdown y lo devuelve en HTML.

fn render(call: Call) -> JsResult<JsString> {
    let scope = call.scope; // obtener el contexto
    let md: Handle<JsString> = try!(try!(call.arguments.require(scope,0)).check::<JsString>()); // obtener primer argumento como JsString. aquí puede hacerse tratamiento de fallos
    let string = md.value(); // Pasar JsString a String
    let html: String = markdown::to_html(&string); // usamos la crate markdown para renderizar a html
    Ok(JsString::new(scope, &html).unwrap()) // devolvemos un JsString con el contenido del HTML
}

Las funciones que interactuan con Node deben devolver un JsResult de un tipo JsXXX, por ejemplo, JsString, JsUndefined o JsInteger. Siempre aceptan un argumento llamado de tipo Call que nos da toda la información necesaria y que podemos usar para sacar argumentos. El scope o contexto es muy importante y lo deberemos usar en las funciones que interactúen con Node.

Código completo del fichero Rust

#[macro_use]
extern crate neon;
extern crate markdown;

use neon::vm::{Call, JsResult};
use neon::js::JsString;
use neon::mem::Handle;

fn render(call: Call) -> JsResult<JsString> {
    let scope = call.scope;
    let md: Handle<JsString> = try!(try!(call.arguments.require(scope,0)).check::<JsString>());
    let string = md.value();
    let html: String = markdown::to_html(&string);
    Ok(JsString::new(scope, &html).unwrap())
}

register_module!(m, {
    m.export("render", render)
});

Y no te olvides de añadir la dependencia markdown al fichero Cargo.toml.

Probándolo

Es muy fácil probarlo. Con el REPL de Node podemos probar partes del módulo antes de publicarlo a npm.

Para abrir el REPL ejecuta Node sin argumentos

node

E introduce línea por línea lo que quieras probar:

var md = require("./");
md.render("__Esto es Markdown__");

Verás el resultado por la pantalla:

Ahora que ya sabemos que funciona podemos publicarlo a npm si queremos con:

npm publish

Aunque recuerda revisar antes el fichero package.json para especificar la licencia y la descripción. Una vez esté publicado su uso en un nuevo proyecto será muy sencillo y de forma transparente se compilará el código nativo.

var md = require("rust-markdown");
var http = require('http');
var fs = require("fs");

var server = http.createServer((req, res) => {
  fs.readFile("index.md","utf-8",function(err,data){
     var html = md.render(data);
     res.statusCode = 200;
     res.setHeader('Content-Type', 'text/html');
     res.end(html);
  });
});

server.listen(8080, "127.0.0.1", () => {
  console.log("Server running");
});



 

 

Crónica de un vídeo de citas célebres

Vuelvo a la carga con un problema que me ha tenido entretenido un rato y que me parece interesante contaros.

Como muchos sabréis, este año me pasé el videojuego The Witness. Es un juego muy interesante y que os recomiendo. El caso es que nunca llegué a escuchar todas las citas célebres del juego, y en YouTube solo encontré vídeos sueltos con subtítulos en… francés. Así que me propuse hacer un vídeo que recogiese todas las citas célebres del juego, con subtítulos en Español. Antes esta situación hay dos opciones:

  • Lo que una persona normal haría sería buscar en la guía las localizaciones de las citas e ir grabándolas.
  • Lo que haría un perturbado sería meterse en los archivos del juego, decodificar los archivos e implementar un complejo sistema para generar el vídeo.

Por supuesto, hice lo último.

thewitnessquotes

Encontrando archivos del juego

Los archivos del juego no están escondidos, se encuentran en un archivo llamado data-pc.zip. Hasta ahí fácil. Una vez dentro encontramos cientos de archivos con unas extensiones peculiares que nos informan de lo que hay dentro (.texture, .lightmap,…). Sin embargo los archivos de sonido no los encontramos de manera sencilla. Necesitan una conversión. Estaba ya buscando soluciones (sospecho que tiene que ver con Audiokinetic Wwise) cuando encontré en reddit a un buen samaritano que había subido, ya decodificados, los archivos de sonido a Mega.

Al ver los archivos observé cantidad de ficheros .WAV y muy poquitos .OGG. Eso ya nos da una pista, pues las citas célebres han tenido que ser codificadas como Ogg, ya que si fuesen con WAV el fichero sería demasiado grande. Extraigo los archivos y borro todos los WAV, pues sé que ahí no están.

Pero no hemos acabado. Los Ogg no solo eran de citas célebres, también había efectos de sonido largos. Afortunadamente, los archivos de efectos de sonido solían llevar un prefijo común (amb_, spec_,…).

Los subtítulos

Tenemos los ficheros de audio de las citas en formato Ogg. Ahora hacen falta los subtítulos. Están fuera de ese fichero ZIP gigante y no es difícil encontrarlos. En concreto el archivo es_ES.subtitles. Sin embargo, una vez lo abres descubres la primera sorpresa. Es un formato del que desconocía su existencia. Os pongo un poco para ver si alguien es capaz de saber el formato:

 

: tagore_voyage

= 1.000000
Creía que mi viaje había llegado al final
al estar al límite de mis fuerzas...

= 6.700000
que el camino ante mí estaba cerrado,
las provisiones agotadas
y que había llegado la hora de refugiarse en la silenciosa oscuridad.

= 17.299999
Pero encuentro que la voluntad no se me acaba,

= 21.299999
y cuando las palabras se apagan en la lengua,
surgen nuevas melodías del corazón;

= 27.200001
y cuando se pierde el antiguo camino,
una nueva tierra revela sus maravillas.

= 32.700001

= 32.709999
Rabindranath Tagore, 1910


: tagore_end

= 1.000000
En mi vida te he buscado con mis canciones.
Fueron ellas las que me guiaron de puerta en puerta,

= 8.800000
y con ellas me he sentido a mí,
buscando y tocando mi mundo.

= 14.400000

= 14.900000
Fueron mis canciones las que me enseñaron
todas las lecciones que he aprendido

= 18.900000
me mostraban caminos secretos,
condujeron mi vista hacia más de una estrella
en el horizonte de mi corazón.

= 25.500000

= 26.100000
Me guiaron todo el día
hacia los misterios del país del placer y el dolor

= 32.500000
y, finalmente,
¿a las puertas de qué palacio me han
guiado en el atardecer al final de mi viaje?

= 39.299999


: tagore_boast

= 1.000000
Presumí ante los hombres de conocerte.

= 4.300000
Ven tus imágenes en mis trabajos.
Vienen y me preguntaban "¿Quién es él?"

= 10.900000
No tengo respuesta para ellos.
Les digo "No sabría decirlo".

= 16.799999
Me culpan y se marchan con desprecio.
Y tú te sientas ahí, sonriendo.

= 23.500000

= 24.700001
Mis historias sobre ti quedan en canciones duraderas.
El secreto sale a borbotones de mi corazón.

= 31.000000
Vienen y me piden
"Cuéntame todo lo que significan".

= 34.900002
No tengo respuesta para ellos.
Les digo "Ah, ¡quién sabrá!"

= 40.500000

= 41.000000
Sonríen y se marchan con desprecio absoluto.
Y tú te sientas ahí, sonriendo.

= 48.000000

= 49.500000
- Rabindranath Tagore, 1910

Pero no me iba a detener. Así que empecé a diseñar un programa que permitiese traducir este archivo a un archivo SRT normal y corriente. Para ello usaría Regex a saco (me leí el libro, para algo me tendría que servir).

regexp

Para hacer el programa usé Node.js. Sí, se que para este tipo de cosas el mejor lenguaje es Perl, o un derivado como Ruby pero todavía no he aprendido lo suficiente de Ruby como para plantearmelo. JavaScript cuenta de forma estándar (tanto Node.js como navegador) con la clase RegExp, que permite ejecutar expresiones regulares y esa es la que he usado.

Finalmente conseguí hacer un script de Node.js, sin dependencias externas, que traduciese este archivo subtitles en un SRT.

Generando un vídeo para cada cita

Ya tenemos el audio, tenemos los subtítulos en un formato conocido. Vamos ahora a generar un vídeo. Primero necesitaremos una imagen de fondo. Pillo una cualquiera de Internet y empiezo a hacer pruebas con ffmpeg. El formato de salida va a ser MP4 codificado con H264 porque realmente es el formato que más rápido se codifica en mi ordenador.

Nada más empezar empiezo a ver que los subtítulos no están funcionando, no se fusionan con la imagen y el audio. Al parecer es un problema que involucra a fontconfig, ffmpeg y Windows. Sí, estaba usando Windows hasta ahora.

Me muevo a Debian y ahora ya funciona bien el fusionado de subtítulos.

Ahora intento unir dos vídeos con ffmpeg también. Fracaso. Lo vuelvo a intentar, FRACASO. Si os digo que la mayor parte del tiempo que me ha llevado este proyecto ha sido encontrar como concatenar varios MP4 en ffmpeg sin que me diese errores extraños quizá no os lo creeríais, pero es verídico. No me creeríais porque la wiki de ffmpeg lo explica correctamente y si buscáis por Internet os van a decir lo mismo. ¿Qué era lo que pasaba?

  1. Las dimensiones de los vídeos no cuadraban
    1. Esto fue obvio y fue lo primero que pensé. ffmpeg tiene un filtro de escalado, pero por alguna razón no funcionaba. La razón era que estaba usando dos veces la opción “-vf” (filtro de vídeo), una con los subtítulos y otra con el escalado. ffmpeg no admite nos veces la opción, si quieres aplicar dos filtros de vídeo tienes que usar una coma entre ellos.
  2. Formato de píxeles
    1. Este era el verdadero problema. Normalmente no suele pasar, pero como las imágenes de los dos vídeos venían de fuentes distintas, ffmpeg usó un formato de píxeles distinto en cada una. Forzando a ffmpeg a usar siempre “yuv420p” funcionó y la concatenación se pudo realizar.

Probé también con mkvmerge, pero me decía que la longitud de los códecs era distinta. No entendí el error hasta que no me enteré que había sido el formato de píxeles, cada vídeo usaba uno distinto en su codificación.

El comando necesario para generar cada vídeo fue entonces:

ffmpeg -loop 1 -i imagen.jpg -i audio.ogg -c:v libx264 -tune stillimage -pix_fmt yuv420p -s 1280x720 -vf scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,subtitles=subtitulos.srt video.mp4

Concatenar los vídeos

Para concatenar los vídeos es necesario tener un archivo de texto donde se indiquen los archivos y su orden, siguiendo este formato:

# Archivo de concatenacion ffmpeg
file 'video1.mp4'
file 'video2.mp4'
file 'video3.mp4'

Luego, su uso es bastante sencillo:

ffmpeg -f concat -i videos.txt -c:v copy video.mp4

El script final

Ahora solo hacía falta convertir todos los archivos de audio en vídeo con sus subtítulos. Usando un script de bash se puede hacer esto:

for f in *.ogg; do
	node main.js "${f%.*}"
done

Y el código de main.js es el siguiente. main.js se encarga de traducir los ficheros subtitles a SRT, de llamar a ffmpeg y de añadir el vídeo a la lista de videos.txt para la posterior concatenación.

var fs = require("fs");
var spawn = require("child_process").spawn;

var ZONE = process.argv[2];
var IMG = process.argv[2]+".jpg";

function timeFormat(SEC,MILISEC){
	var date = new Date(SEC * 1000);
	var regex = new RegExp("([0-9]+:[0-9]+:[0-9]+)");
	var str = regex.exec(date.toUTCString())[1] + "," + MILISEC.substring(0,3);
	return str;
}

var witness = fs.readFileSync("es_ES.subtitles","utf-8");
var srt = fs.createWriteStream(ZONE+".srt");

var regex = ": "+ZONE+"\n\n= ([0-9]+).([0-9]+)\n";

var LINE = 1;

while(!(new RegExp(regex + "\n: ","g").test(witness))){
	var start = new RegExp(regex,"g");
	var match = start.exec(witness);
	
	var START_TIME_SEC = parseInt(match[match.length - 2]);
	var START_TIME_MILISEC = match[match.length - 1];
	var TEXT = "";

	do{
		var text = new RegExp(regex + "(.+)\n","g");
		var exec = text.exec(witness);
		if(exec!=null){
			TEXT = TEXT + "\n" + exec[exec.length - 1];
			regex = regex + "(.+)\n";
		}
	}while(exec != null);
	
	regex = regex + "\n= ([0-9]+).([0-9]+)\n";
	var time = new RegExp(regex,"g");
	var end_time = time.exec(witness);
	if(end_time != null){
		var END_TIME_SEC = parseInt(end_time[end_time.length - 2]);
		var END_TIME_MILISEC = end_time[end_time.length - 1];
		srt.write(LINE + "\n");
		srt.write(timeFormat(START_TIME_SEC,START_TIME_MILISEC)+" --&amp;amp;amp;gt; "+timeFormat(END_TIME_SEC,END_TIME_MILISEC));
		srt.write(TEXT);
		srt.write("\n\n");
	}else{
		srt.write(LINE + "\n");
		srt.write(timeFormat(START_TIME_SEC,START_TIME_MILISEC)+" --&amp;amp;amp;gt; "+timeFormat(5*60,"000000"));
		srt.write(TEXT);
		srt.write("\n\n");
		break;
	}
	LINE++;
}

srt.end();

var ffmpeg = spawn("ffmpeg",["-loop","1","-i",IMG,"-i",ZONE+".ogg","-c:v","libx264","-tune","stillimage","-pix_fmt","yuv420p","-s","1280x720","-shortest","-vf","scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,subtitles="+ZONE+".srt",ZONE+".mp4"]);

ffmpeg.stderr.on('data', (data) =&amp;amp;amp;gt; {
  console.log(`stderr: ${data}`);
});

fs.appendFileSync("video.txt","file '"+ZONE+".mp4'\n");

Se trata de un programa que hice deprisa y corriendo y aunque el código es feo (o eso me parece a mi), la verdad es que me ha servido.

Y el resultado…


 

Tutorial de WebExtensions (II) – Popup y Tabs

Continuamos con el tutorial de WebExtensions, hoy veremos un elemento muy útil para muchas extensiones que es el popup.

¿Qué es el popup?

El popup es un panel desplegable que se acciona al pulsar un botón de acción. Este panel es una página HTML que puede contener código JavaScript.

popup

El popup es el elemento que permite al usuario un mayor control sobre la extensión y es ideal tanto para mostrar información como para contener acciones. De hecho, WebExtensions limita el número de botones de acción que puede crear una extensión a uno. Si queremos simular tener más botones deberemos usar un popup que despliegue un pequeño menú.

Definir el popup en manifest.json

Para definir el popup tenemos que indicar la ubicación del fichero HTML correspondiente al popup.

	"browser_action" : {
		"default_icon" : "icons/icono_guay.png",
		"default_title" : "Mi PopUp guay",
		"default_popup" : "popup/index.html"
},

Con eso ya tenemos definido el popup. Se mostrará el botón en la barra superior y al hacer click se abrirá el popup.

El PopUp en sí

El PopUp es un fichero HTML sin el mayor misterio. Para definir el tamaño del PopUp usaremos CSS:

body{
    width: 400px;
    height: 400px;
}

Y el navegador adaptará el PopUp a esas dimensiones. A ese archivo HTML le podemos añadir archivos JavaScript mediante el procedimiento habitual en la web, vía una etiqueta script. Lo interesante viene en estos archivos de JavaScript pues cuentan con más privilegios. Desde ellos podemos acceder a las APIs retringidas a extensiones así como librarnos de limitaciones impuestas a las webs (como la política que bloquea los XMLHttpRequests ejecutados a dominios ajenos a nuestra página).

Para usar las APIs restringidas debemos pedir permiso en el fichero manifest.json. Por ejemplo, vamos a jugar con las pestañas. Para ello usamos los permisos tabs y activeTab.

    "permissions" : ["tabs","activeTab"],

Tabs (pestañas) en WebExtensions

La API se encuentra en chrome.tabs y dispone de varios métodos y eventos. Vamos a ver los más importantes:

  • chrome.tabs.query – Obtener información de las pestañas
    • Este método es muy flexible, lo podemos comparar a hacer un SELECT en SQL, pues obtiene una lista de pestañas que cumplen los parámetros que hemos indicado. Por ejemplo para comprobar la pestaña actual, usaremos la propiedad active:
      chrome.tabs.query({"active": true},function(tabs){
          var tab = tabs[0];
          // tab es la pestaña actualmente activa 
      });
      

      Otra opción muy interesante es para averiguar que pestañas tienen abierta una URL que cumple un patrón. Además los parámetros de búsqueda se pueden combinar:

      chrome.tabs.query({"url" : "*://*.adrianistan.eu/*", "status" : "loading", "muted" : true},function(tabs){
        // tabs (que puede estar vacío) contiene todas las URL que estan en un adrianistan.eu (subdominios incluidos),
        // se encuentran cargando
        // y el usuario ha silenciado su sonido
      });
      
  • chrome.tabs.create – Permite crear nuevas pestañas
    • Su uso básico es muy simple
      chrome.tabs.create({"url" : "https://blog.adrianistan.eu/"},function(tab){
        // tab es la pestaña creada
      });
      
  • chrome.tabs.executeScript – Esta función permite inyectar a la pestaña un script de contenido (Content Script).
    • Ahora no vamos a entrar en como se realizaría la comunicación entre los scripts Background y los Content. El código se puede pasar por una cadena de texto o por un archivo, siendo preferible esta última opción. Se puede especificar además cuando queremos que se ejecute dentro del ciclo de carga de la pestaña.
      chrome.tabs.executeScript(tab.id (opcional), {"code" : "alert('Hola');"},function(){
      
      });
      
      O
      
      chrome.tabs.executeScript({"file" : "miarchivo.js", "runAt" : "document_start"},function(){
      
      });
      
  • chrome.tabs.insertCSS – Permite inyectar CSS en la pestaña
    • Su uso es exactamente igual al de chrome.tabs.executeScript
      chrome.tabs.insertCSS({"file" : "misestilos.css"},function(){
      
      });
      
  • chrome.tabs.sendMessage – Enviar un mensaje a la pestaña
    • Hasta ahora no hemos visto la separación entre background y content scripts. Esta función permite enviar datos desde los scripts background a los content, que tendrán una función de escucha. La menciono, pero lo veremos más adelante en profundidad.

Por último, veamos algunos de los eventos que puede generar la API.

  • chrome.tabs.onUpdated – Es llamado cada vez que una pestaña sufre una modificación en sus datos (cambio de URL, quitar el sonido, finalizar una carga, etc)
    • Es muy posible que necesitemos filtrar el contenido que nos da, para fijarnos si se ha producido en una URL que nos interesa o en alguna condición relevante. Eso no obstante, es responsabilidad del programador.
      chrome.tabs.onUpdated.addListener(function(tabId,changeInfo,tab){
        // el objeto changeInfo presenta una estructura similar al de búsqueda con chrome.tabs.query
        // este objeto solo contiene valores para aquellos parámetros que han cambiado
        // con tab podemos acceder a la pestaña al completo
      });
      

Con esto ya podemos hacer extensiones interesantes, la APIs de PopUp y de Tabs son de las más usadas en WebExtensions.

Tutorial de WebExtensions (I) – manifest.json y conceptos

WebExtensions es un sistema para desarrollar extensiones de navegador. Realmente se trata de la API original de Google Chrome y Chromium. Esta API ya había sido diseñada teniendo en cuenta las extensiones de Firefox basadas en XUL y XPCOM. Simplifica mucho el proceso de tareas comunes, no requiere reinicios y es más segura al no permitir tanto control sobre el navegador.

WebExtensions
Tomada de http://tenfourfox.blogspot.com.es/2015/08/okay-you-want-webextensions-api.html

Posteriormente Firefox respondería con Jetpack y Addon SDK, un sistema similar pero escrito encima de XUL y XPCOM, incompatible pero que no necesitaba reinicios y tampoco perdía potencia pues las APIs de XPCOM seguían allí.

Luego Opera decide abandonar Presto y usar Blink, el motor de Chromium. Con respecto a su sistema de extensiones, planean ser 100% compatibles con Chrome.

Firefox también ha de realizar cambios internos, concretamente se inició el trabajo (electrolysis) de hacer Firefix multiproceso. Esto rompía gran parte de las APIs internas de Firefox. Todos los complementos necesitarían ser reescritos. Ya de paso, teniendo en cuenta que hay que diseñar una nueva API para extensiones, se toma la de Chrome y se renombra WebExtensions. Posteriormente Microsoft afirma que Edge será compatible con extensiones de Chrome o WebExtensions. Esta es la historia de como 4 navegadores se pusieron de acuerdo en estandarizar la API de desarrollo de extensiones para navegadores.

En esta serie de tutoriales vamos a ver como programar nuestra extensión usando WebExtensions.

manifest.json

El fichero manifest.json de una extensión es muy importante y es obligatorio. Contiene metadatos de la extensión así como permisos de los que dispone la extensión y las acciones que define.

En primer lugar vamos a distinguir entre distintos tipos de scripts distintos.

  • Background scripts: estos scripts se ejecutan en el fondo del navegador. Se ejecutan al iniciar el navegador. Podemos configurar eventos y entonces se mantendrán a la escucha durante toda la sesión. Estos scripts no pueden acceder a las páginas web.
  • Content scripts: estos scripts se añaden a determinadas páginas según un patrón en el archivo manifest.json o desde un background script. Estos scripts interactúan directamente con las páginas en los que han sido adjuntados.

Veamos ahora un fichero manifest.json

 

{
	"manifest_version" : 2,
	"name" : "Bc.vc shortener",
	"version" : "2.0.0",
	"description" : "Shorthener for Bc.vc",
	"homepage_url" : "http://adrianistan.eu/norax/",
	"icons" : {
		"16" : "icon-16.png",
		"48" : "icon-48.png",
		"64" : "icon-64.png",
		"96" : "icon-96.png",
		"128" : "icon-128.png"
	},
	"applications" : {
		"gecko" : {
			"id" : "@bc-vc",
			"strict_min_version" : "45.*"
		}
	},
	"permissions" : ["notifications","contextMenus","clipboardWrite","activeTab","storage","http://bc.vc/"],
	"browser_action" : {
		"default_title" : "Bc.vc shortener",
		"default_icon" : "icon-64.png"
	},
	"background" : {
		"scripts" : ["background/install.js","background/main.js"]
	},
	"options_ui" : {
		"page" : "options/options.html",
		"chrome_style" : true
	}
}

manifest_version siempre es 2. Al principio rellenamos metadatos sencillos, name, description, version, homepage_url,… Posteriormente debemos añadir los iconos. Dependiendo de lo que queramos no hacen falta todos los iconos. Por ejemplo, el icono de 16×16 solo es necesario si nuestra extensión define un menú contextual del botón derecho. La sección applications ha sido añadida por Mozilla y originalmente no está en Chrome. Define la compatibilidad entre distintos navegadores y versiones, sin embargo, por ahora solo Firefox tiene en cuenta esto.

A continuación se definen los permisos. Una extensión necesita pedir permiso para acceder a ciertas APIs más privilegiadas. Por ejemplo, para mostrar notificaciones o para realizar peticiones HTTP a servidores remotos.

A continuación vemos la acción del navegador. A diferencia de otros sistemas de extensiones, WebExtensiones solo permite un botón en la interfaz del navegador. Es una manera excelente de separar funcionalidades, aunque si vienes de Firefox verás como limita a muchas extensiones, algunas de las cuáles modificaban la interfaz por completo. La acción del navegador puede hacer dos cosas: abrir un panel popup o generar un evento en el background script. Dependiendo de como sea la experiencia de usuario que prefieras te convendrá una opción u otra (en este caso, como no hemos definido un popup html default_popup, tendremos que escuchar el evento browserAction en un background script). Similar a browser_action existe page_action, la única diferencia es el lugar donde aparece el botón.

Page Action a la izquierda
Browser Action a la derecha

Después podemos asignar archivos de background scripts. Por último options_ui es la manera adecuada de crear una página de opciones. Será una página HTML, pero de eso hablaremos en otro tutorial.

Por último vamos a ver los content scripts

"content_scripts": [
  {
    "matches": ["*://*.mozilla.org/*"],
    "js": ["borderify.js"],
    "css" : ["style.css"],
    "all_frames" : false,
    "run_at" : "document_end"
  }
]

Una manera de añadir content scripts es desde el manifest.json, especificando un patrón en matches. Podemos añadir JavaScript y CSS.

Otra opción es web_accesible_resources. Se trata de elementos de la extensión que pueden ser cargados desde una página web. Por ejemplo, si un content script quiere mostrar una imagen en una web, esta deberá estar indicada en web_accesible_resources.

"web_accesible_resources" : ["imagen/zorro.png"]

...

chrome.extension.getURL("imagen/zorro.png"); // desde el content script

Este fichero es el más importante de una WebExtension. En el siguiente tutorial veremos los background scripts combinados con browser actions.