Blog

How To: JavaScript-Module (*.jsm) und clipboard-API mit dem Add-on SDK

Verfasst von: Sören Hentzschel

How To: JavaScript-Module (*.jsm) und clipboard-API mit dem Add-on SDK

Im ersten Tutorial zum Add-on SDK von Firefox haben wir eine Einführung in die Add-on-Erstellung erhalten und bereits verschiedene APIs genutzt: Wir haben gelernt, Funktionen in der Add-on Leiste von Firefox hinzuzufügen (Widget-API) und Seiten auf verschiedene Weisen zu öffnen (Tabs-API). Durch die Self-API waren wir in der Lage, Dateien mit unserer Erweiterung mitzuliefern und schließlich konnten wir durch das preferences-service-Modul auch noch Entscheidungen in Abhängigkeit von about:config-Schaltern treffen. Im zweiten Tutorial habe ich gezeigt, wie man Zugriff auf das Components-Objekt von Firefox erhält, welches uns Zugriff auf sämtliche XPCOM-Interfaces gibt, was beinahe unbegrenzte Möglichkeiten eröffnet.

Heute lernen wir eine weitere Möglichkeit kennen, Komponenten von Firefox zu nutzen, nämlich in Form von JavaScript-Modulen (*.jsm). Außerdem lernen wir noch eine weitere API kennen: die Clipboard-API, um Inhalte in die Zwischenablage zu kopieren.

Beginnen wir direkt mit den JavaScript-Modulen. Wir erinnern uns an das letzte Tutorial zurück, in welchem wir folgenden Code benutzt haben, um die Datei application.ini aus dem Installationsverzeichnis von Firefox zu laden, um diese anschließend durch einen INI-Parser auszulesen:

var applicationFile = Cc['@mozilla.org/file/directory_service;1'].getService(Ci.nsIProperties).get('CurProcD', Ci.nsIFile);
applicationFile.append('application.ini');

XPCOM stellt einen mächtigen Weg dar um Zugriff auf sehr viel Firefox-Funktionalität zu erhalten, vieles ist stattdessen aber auch als JavaScript-Modul implementiert. Diese Aufgabe können wir sowohl über den XPCOM-Weg als auch über den JSM-Weg lösen. Die Dokumentation hatte uns an dieser Stelle bereits eine alternative Möglichkeit über ein solches JavaScript-Modul, nämlich FileUtils.jsm, vorgeschlagen:

let FileUtils = Cu.import('resource://gre/modules/FileUtils.jsm').FileUtils;
var applicationFile = FileUtils.getFile('CurProcD', ['application.ini']);

Tauschen wir diese beiden Zeilen aus, funktioniert unsere Erweiterung ganz genau so wie vorher. Die JSM-Version ist nicht nur schöner zu lesen, sondern auch vorzuziehen, sofern möglich.

Das als kleine Einführung. Wir wollen für unser nächstes kleines Erweiterungsprojekt nämlich ein JavaScript-Modul nutzen. Zuerst wieder die Konzeptionelle Frage: Was wollen wir machen? Dieses mal bauen wir eine leicht veränderte Version meiner Erweiterung Copy Extensions to Clipboard. Diese Erweiterung erzeugt wieder ein Symbol in der Add-on Leiste von Firefox. Bei Klick auf dieses wird eine Auflistung aller installierten Erweiterungen in die Zwischenablage kopiert. Dazu beginnen wir wieder mit einem Grundgerüst, welches wir dieses mal extensions taufen und die (noch) leere Methode copyExtensions() beinhaltet. Außerdem erstellen wir gleich unser Icon für die Add-on Leiste, welches diese Methode bei Klick aufruft, und laden über das [+]-Icon unter Data ein passendes Icon dazu hoch. Nachdem die Widget-API bereits Teil beider vorangegangener Tutorials war, erspare ich mir hierzu weitere Kommentare:

const self = require('self');

var extensions = {
    copyExtensions : function() {
        
    }
}

exports.main = function() {    
    var widget = require('widget').Widget({
        id : 'extensions',
        label : 'copy extensions to clipboard',
    	contentURL : self.data.url('extension.png'),
		onClick : function() {
    		    extensions.copyExtensions();
		}
	});
};

Um auf alle installierten Erweiterungen zugreifen zu können, benutzen wir das AddonManager.jsm-Modul. Dieses importieren wir auf die gleiche Weise wie obiges Beispiel. Praktischerweise zeigt die Dokumentation wieder direkt den Code, den wir brauchen und auch gleich mit getAllAddons() die richtige Methode.

copyExtensions : function() {
    let AddonManager = Cu.import('resource://gre/modules/AddonManager.jsm').AddonManager;
        
    AddonManager.getAllAddons(function(aAddons) {
        
    });
}

Dabei vergessen wir natürlich zu Beginn des Scripts nicht:

const {Cu} = require('chrome');

Nun bedarf es einer kurzen Überlegung: Wir wollen Informationen zu jeder installierten Erweiterung haben. Und diese wollen wir in die Zwischenablage kopieren. Über getAllAddons() erhalten wir Informationen zu allen installierten Erweiterungen, das Array aAddons beinhaltet dabei gemäß Dokumentation alle Add-on-Objekte. Wir können also über dieses Array iterieren und mit den Informationen etwas anstellen. In jedem Fall brauchen wir am Ende etwas vom Typ String, was wir in die Zwischenablage kopieren können (kleine Vorwegname zur clipboard-API). Die Idee ist es, die Informationen zu jeder Erweiterung in einen String zu verpacken und damit ein Array aufzufüllen, welches die Informationen zu jeder Erweiterung beinhaltet. Anschließend werden wir aus diesem Array wieder einen einzigen String machen, so dass wir die Informationen in die Zwischenablage kopieren können.

Als erstes erstellen wir dazu in der getAlladdons()-Methode ein neues Array extensionList für unsere Erweiterungen. In einer for-Schleife weisen wir einer addon-Variablen das jeweilige Add-on-Objekt zu und erstellen außerdem noch eine Variable für den dazugehörigen Inhalt, welchen wir später kopieren möchten. Über addon.eigenschaft können wir dann auf die verschiedenen Informationen zu den Erweiterungen zugreifen. Die möglichen Eigenschaften stehen hier. Wir entscheiden uns für den Namen und die Version der Erweiterung. Schließlich füllen wir mit diesem Eintrag in jedem Schleifendurchlauf unser Array mit extensionList.push(entry). Und weil wir an dieser Stelle testen möchten, ob wir bis hierhin alles richtig gemacht haben, machen wir eine Konsolen-Ausgabe. Auch hierfür benötigen wir einen String. Dazu können wir außerhalb der Schleife console.error(extensionList.toString()) verwenden. Doch wenn wir das jetzt testen, werden wir feststellen, dass es zwar funktioniert, aber alle Einträge komma-seperiert nebeneinander stehen. Schöner wäre es da doch, wenn alles untereinander stünde. Statt extensionList.toString() verwenden wir stattdessen extensionList.join(‘\r\n’). Wichtig ist dabei, dass man nicht nur \n verwendet, da im Windows Editor nach dem Kopieren später sonst trotzdem alles nebeneinander stehen wird. Was wir gerade gemacht haben:

AddonManager.getAllAddons(function(aAddons) {
    var extensionList = [];
            
    for(var i in aAddons) {
        var addon = aAddons[i];
        var entry = '';
               
        entry = addon.name + ', Version: ' + addon.version;
                
        extensionList.push(entry);
    }
            
    console.error(extensionList.join('\r\n'));
});

Die Reihenfolge der Erweiterungen ist noch ziemlich wild. Wir wollen unsere Erweiterungen alphabetisch sortieren. Dies machen wir, indem wir vor der Ausgabe noch folgendes einfügen: extensionList.sort(). Hat man Erweiterungen installiert, welche sowohl mit kleinen als auch mit großen Buchstaben beginnen, fällt es vielleicht an dieser Stelle auf: Die Erweiterungen sind zwar nun alphabetisch sortiert, aber case-sensitive, das bedeutet, dass erst alle Erweiterungen kommen, welche mit einem Großbuchstaben beginnen, anschließend erst alle, welche mit einem Kleinbuchstaben beginnen. Um korrekt alphabetisch zu sortieren, müssen wir in den Sortier-Algorithmus eingreifen und der sort()-Funktion noch eine Vergleichsfunktion mitgeben. Die Vergleichsfunktion muss einen Wert kleiner null, null oder größer null zurückliefern, je nachdem, ob der zweite Parameter der Vergleichsfunktion größer, gleich oder kleiner dem ersten ist, wobei die Parameter den Zeichen entsprechen, welche miteinander verglichen werden. Dazu verwenden wir die toLowerCase()-Funktion, um beide Buchstaben sicher als Kleinbuchstaben zu erhalten und überprüfen, ob die beiden Kleinbuchstaben gleich oder ungleich sind. Sind sie ungleich, wird -1 zurückgegeben, wenn der erste kleiner ist, und 1, wenn der zweite kleiner ist. Sind die Kleinbuchstaben hingegen miteinander identisch, wird überprüft, ob die originalen Buchstaben identisch sind und in diesem Fall eine 0 zurückgegeben und ansonsten wieder -1 oder 1, je nachdem, welcher Buchstabe kleiner oder größer ist, verglichen mit den originalen Buchstaben. Dies mag jetzt erst einmal kompliziert klingen, ist es aber gar nicht, wenn man den Code einmal genauer ansieht:

extensionList.sort(function(a, b) {
    var al = a.toLowerCase(), bl = b.toLowerCase();
                
    if(al == bl) {
        if(a == b) {
            return 0;
        }
        else {
            if(a < b)
                return -1;
            else
                return 1;
            }
        }
    else {
        if(al < bl)
           return -1;
        else
            return 1;
    }        
});

Und wer es lieber kompakt mag, kann dies auch kürzer schreiben:

extensionList.sort(function(a, b) {
    var al = a.toLowerCase(), bl = b.toLowerCase();
                
    return al == bl ? (a == b ? 0 : a < b ? -1 : 1) : al < bl ? -1 : 1;
});

Wenn wir unser Add-on nun noch einmal testen, stimmt nun auch die Reihenfolge der Erweiterungen!

Nun geben wir vor dem Versionsnamen noch den Typ der Erweiterung an und ggf. ob die Erweiterung gerade deaktiviert ist, dafür überprüfen wir addon.type und addon.isActive. Die Zeile mit den Add-on-Informationen, die wir schon im Code stehen haben, müssen wir dann noch leicht anpassen und aus dem = ein += machen, damit wir den String nur ergänzen und nicht überschreiben:

switch(addon.type) {
    case 'extension':
        entry = '[extension]';
        break;
    case 'plugin':
         entry = '[plugin]';
         break;
    case 'theme':
    	entry = '[theme]';
    	break;
    case 'user-script':
    	entry = '[user-script]';
    	break;
    case 'userstyle':
        entry = '[userstyle]';
        break;
    default:
    	entry = '[unknown]';
}
                
if(!addon.isActive)
    entry += '[DISABLED]';
                
entry += ' ' + addon.name + ' ' + addon.version;

Wir könnten den addon.type auch direkt verwenden und uns diesen ganzen switch-Block sparen. Der Grund dafür, dass wir es so vermeintlich umständlich machen, ist der, dass wir die Erweiterung für das nächste Tutorial weiterverwenden wollen. Dort lernen wir dann, wie man mit dem SDK erstellte Erweiterungen in mehrere Sprachen lokalisiert. :)

Jetzt werden wir die Konsolen-Ausgabe durch ein Kopieren in die Zwischenablage ersetzen. Dafür binden wir als erstes die clipboard-API ein:

const clipboard = require('clipboard');

Nun ersetzen wir console.log() nur noch durch clipboard.set() und schon steht der Inhalt, welcher vorher in die Konsole geschrieben wurde, in der Zwischenablage! Wir erweitern das Ganze jetzt noch, indem wir den Inhalt zusätzlich in einem neuen Tab noch anzeigen. Dafür schreiben wir unter dem clipboard.set() noch:

const tabs = require('tabs').open('data:text/plain;charset=utf-8,' + extensionList.join('%0A'));

Die Tabs-API kennen wir ja schon, eine kurze Erklärung ist trotzdem notwendig. Mit der open()-Methode öffnen wir einen neuen Tab und können durch das data:text/plain Text direkt über die Adresszeile mitgeben, welchen wir in Firefox anzeigen. Das charset=utf-8 ist notwendig, damit Umlaute und Sonderzeichen korrekt dargestellt werden. Außerdem fällt auf, dass wir hier auch wieder extensionList.join() verwenden, allerdings übergeben wir als Parameter hier nicht ‘\r\n’, sondern ‘%0A’. Das hat den Grund, dass wir den String über die Adressleiste übergeben, dort werden neue Zeilen auf diese Weise dargestellt, ‘\r\n’ wäre an dieser Stelle wirkungslos.

Der komplette Code:

const {Cu} = require('chrome');
const clipboard = require('clipboard');
const self = require('self');

var extensions = {
    copyExtensions : function() {
        let AddonManager = Cu.import('resource://gre/modules/AddonManager.jsm').AddonManager;
        
        AddonManager.getAllAddons(function(aAddons) {
            var extensionList = [];
            
            for(var i in aAddons) {
                var addon = aAddons[i];
                var entry = '';
                
                switch(addon.type) {
                    case 'extension':
                    	entry = '[extension]';
                		break;
                	case 'plugin':
                		entry = '[plugin]';
                		break;
                	case 'theme':
                		entry = '[theme]';
                		break;
                	case 'user-script':
                		entry = '[user-script]';
                		break;
                	case 'userstyle':
                		entry = '[userstyle]';
                		break;
                	default:
                		entry = '[unknown]';
                }
                
                if(!addon.isActive)
                    entry += '[DISABLED]';
                
                entry += ' ' + addon.name + ' ' + addon.version;
                
                extensionList.push(entry);
            }
            
            extensionList.sort(function(a, b) {
                var al = a.toLowerCase(), bl = b.toLowerCase();
                
                return al == bl ? (a == b ? 0 : a < b ? -1 : 1) : al < bl ? -1 : 1;
            });
            
            clipboard.set(extensionList.join('\r\n'));
            const tabs = require('tabs').open('data:text/plain;charset=utf-8,' + extensionList.join('%0A'));
        });
    }
}

exports.main = function() {    
    var widget = require('widget').Widget({
        id : 'extensions',
        label : 'copy extensions to clipboard',
    	contentURL : self.data.url('extension.png'),
		onClick : function() {
    		extensions.copyExtensions();
		}
	});
};
3


Kommentare

  1. Marius Cramer  24. Februar 2012, 15:56

    Ich glaub da ist ein kleiner “Logikfehler” im Code ;)

    case ‘user-script':
    entry = ‘[extension]';
    break;
    case ‘userstyle':
    entry = ‘[user-script]';
    break;
    default:
    entry = ‘[userstyle]';

    Da bist du wohl um jeweils eine Zeile verrutscht bei den entry Einträgen

  2. Sören Hentzschel  24. Februar 2012, 19:12

    Uh, ich hätte auch nochmal über den Code lesen sollen und nicht nur über den Text. Danke dir, ist korrigiert. :)

Kommentar hinzufügen

E-Mail-Benachrichtigung bei weiteren Kommentaren.