29.102014

Plugins fuer metalsmith entwickeln

Metalsmith bekommt ein Quell- und ein Zielverzeichnis angegeben; bearbeitet u.U. alle Dateien, die im Quellverzeichnis zu finden sind und schreibt das Ergebnis in das Zielverzeichnis. Das Projekt stellt sich dabei selbst als Generator für statische Webseiten vor. Das ist aber quasi typisch britisches Understatement, denn im Prinzip lässt sich alles, was die Transformation von Dateien betrifft, mit Metalsmith automatisieren, im Zweifel muss nur das passende Plugin geschrieben werden. Wie einfach das geht, zeigt ein Blick auf das 'convert' Plugin. Das convert Plugin benutzt Imagemagick um Bilddateien zwischen Formaten zu konvertieren und/oder ihre Größe zu ändern. Metalsmith ist eine node.js Anwendung und als solche sehr einfach zu installieren:

npm install metalsmith

Metalsmith arbeitet sehr simpel:

  1. Alle Dateien werden in ein Array eingelesen, dass Dateinamen (relative Pfade) mit einem Array das Metadaten und den Dateiinhalt hält
  2. Konfigurierte Plugins werden der Reihe nach mit diesem Array als Parameter aufgerufen; sie verändern dieses Array dabei ganz nach ihrem Gusto.
  3. Das nun alle Ergebnisse haltende Array wird auf die Platte geschrieben

Metalsmith kann dabei programmatisch aus node.js-Anwendungen oder per json-Datei konfiguriertes CLI-Werkzeug benutzt werden. Plugins bestehen aus einer Funktion, die die Konfiguration des Plugins übergeben bekommt und eine Funktion zurückgibt, die mit dem Dateiarray, metalsmith-Metainformationen und einer Callbackfunktion aufgerufen werden wird. Klingt kompliziert? Ist es nicht! Schauen wir uns das Plugin Schritt für Schritt an: Im ersten Schritt werden alle Module herangezogen, die von unserem Plugin benutzt werden. 'debug' erlaubt es vom Benutzer steuerbare Meldungen auszugeben, 'path' stellt Funktionen zur Manipulation und Analyse von Dateipfaden bereit, 'minimatch' ist ein API zur Verwendung regulärer Ausdrücke. 'imagemagick-native' ist die Bibliothek, die den Zugriff auf Imagemagick aus nodejs heraus kapselt -- 'util' schließlich ist das Sammelbecken für Funktionalitäten in nodejs, die nicht sinnvoll anderen Paketen zugeordnet werden können aber auch kein eigenes Paket rechtfertigen.

var debug = require('debug')('metalsmith-convert'),
    path = require('path'),
    minimatch = require('minimatch'),
    im = require('imagemagick-native'),
    util = require('util');

Der zweite Schritt exportiert die Funktion, die die Pluginfunktion zurückgibt:

module.exports = convert;

Diese Funktion definieren wir nun, wird sie Aufgerufen gibt sie die eigentliche Pluginfunktion zurück

function convert(options) {
  return function(files, metalsmith, done) {

In 'results' merken wir uns Dateinamen, die als Resultat der Ausführung dieses Plugins in 'files' erzeugt wurden, um kaskadierende Bearbeitung von Ergebnisdateien vermeiden zu können. 'ret' wird hier definiert, damit ein Fehler aus einem Durchlauf des Plugin korrekt gemeldet werden kann, ohne andere Durchläufe zu beeinträchtigen.

    var results = {}; // don't process results of previous passes
    var ret = null; // what to return;

Das Plugin erlaubt mehrere Durchläufe zu konfigurieren, die Funktion, die an 'pass' gebunden wird, arbeitet einen solchen konfigurierten Durchlauf ab.

    var pass = function(args) {

Wenn die minimal benötigten Parameter nicht übergeben wurden, wird dieser Durchlauf mit einer Fehlermeldung abgebrochen.

      if (!args.src && !args.target) {
        ret = new Error('metalsmith-convert: "src" and "target" args required');
        return;
      }

Die Zieldateiendung kann explizit übergeben werden, oder wird aus dem ImageMagick Zielformat gebildet

      var ext = args.extension || '.' + args.target;

Jede Datei wird geprüft

      Object.keys(files).forEach(function (file) {

Wenn der Name dem mit 'src' übergebenen Muster entspricht

      if (minimatch(file, args.src)) {

und die Datei nicht das Ergebnis einer Operation dieses Plugin ist

          if (results[file]) return  ;

Dann beginnen wir mit der Bearbeitung, bauen je nach gegebenen Optionen die Argumente für den ImageMagick Aufruf zusammen und führen die Konversion durch.

          var convertArgs = {
            srcData: files[file].contents,
            format: args.target,
            strip: true,
            quality: 90
          };
          var currentExt = path.extname(file);
          var newBasename = path.basename(file, currentExt);

          if (args.resize) {
            convertArgs['width'] = args.resize.width;
            convertArgs['height'] = args.resize.height;
            newBasename = newBasename + "_" + args.resize.width + "_" + args.resize.height;
            debug("Resizing (" + args.resize.width + "x" + args.resize.height + ")");
          }
          var newName = path.join(path.dirname(file), newBasename + ext);
          var result = im.convert(convertArgs);

Dann nehmen wir das Ergebnis als neue Datei in das 'files'-Array auf und merken uns den neuen Dateinamen als Ergebnis der Arbeit dieses Plugin

          files[newName] = { contents: result };
          results[newName] = true;

Wenn gewünscht kann die Quelldatei aus dem Ergebnis entfernt werden

          if (args.remove) {
            delete files[file];
          }
        }});
    };

Damit ist die Funktion, die die eigentliche Arbeit verrichtet komplett. Wenn uns ein Array von Options übergeben wurde, rufen wir 'pass' für jedes Element darin einzeln auf, sonst einmal.

    if (util.isArray(options)) {
      options.forEach(function(opts) {
        pass(opts);
      })
    } else {
      pass(options);
    }

Zum Abschluss müssen wir die übergebene Callbackfunktion aufrufen, die Metalsmith signalisiert, dass diese Plugin seine Arbeit beendet hat. Ist 'ret' nicht 'null', sondern ein Object vom Typ 'Error', wird Metalsmith den Fehler melden und die Arbeit abbrechen, sonst das nächste Plugin aufrufen.

    return done(ret);
  }
}

metalsmith-convert ist mit Version 0.0.5 im Alphastadium, und kann bestimmt noch einiges an Politur und weiterer Funktionalität vertragen, ist in diesem Zustand aber bestens geeignet, zu zeigen, wie ein Metalsmith-plugin zu entwickeln ist.