Egyszerű Syntax Highlighter Drupal 5 alatt

Most már egy ideje működik egy új kód színező a poetro.hu alatt, ez pedig a SyntaxHighlighter JavaScript kódra épül, amit az oldalba integráltam. Ez a kód színező elég sok mindent tud, ugyanakkor pár dologgal nem értek egyet abban, ami az eredeti kódban van, ezért pár dolog máshogy működik. Sajnos a kód Drupal 5-höz készült de mivel nem tartalmaz sok kódot könnyen portolható újabb változatokra.

Az alap, ahogy régen is, a Code Filter modulra épül, azonban ebben kellett végeznem pár módosítást. Amit szerettem volna megvalósítani, az az, hogy tetszőleges CSS osztályt lehessen társítani a kód blokkhoz például <code class="brush-JScript">var x;</code> formában.

Ami viszont teljesen új, az egy modul, ami ezeket a <code> blokkokat kezeli. Maga a PHP kód igazából nem csinál sokat, csak a beállításokat teszi elérhetővé JavaScriptben és betölti a fő szkriptet.

$path = drupal_get_path('module', 'syntax_highlighter');  
drupal_add_js(array(  
'syntax_highlighter' => array(  
'selector' => variable_get('syntax_highlighter_selector', 'code'),  
'path' => base_path() . $path .'/syntaxhighlighter',  
)  
), 'setting');  
drupal_add_js($path .'/syntax_highlighter.js');  

Amint látszik a SyntaxHighlighter JavaScript kódjából nem töltődik be semmi, csak a modul saját JavaScriptje, ami majd betölti a tényleges JavaScript kódot. Ez azért hasznos, mert nem töltődik be feleslegesen több kilobyte-nyi kód. Amennyiben mégis szükség van rá, akkor ráadásul aszinkron módon töltjük be a kódot, ami mivel párhuzamosan több kód töltődik, ezért még gyorsabban is fog tudni betöltődni. Sajnos ezt csak a DOM betöltődése után tudjuk megtenni.

// Global Killswitch
if (Drupal.jsEnabled) {
  (function ($, window, document, undefined) {
    // All available brushes for highlighting.
    var brushes = [
      "AppleScript",
      "AS3",
      "Bash",
      "ColdFusion",
      "Cpp",
      "CSharp",
      "Css",
      "Delphi",
      "Diff",
      "Erlang",
      "Groovy",
      "Java",
      "JavaFX",
      "JScript",
      "Perl",
      "Php",
      "Plain",
      "PowerShell",
      "Python",
      "Ruby",
      "Sass",
      "Scala",
      "Sql",
      "Vb",
      "Xml"
    ],
    init = function () {
      var settings = Drupal.settings.syntax_highlighter,
          bl = brushes.length,
          classesRx,
          i = 0,
          codes = $(settings.selector),
          toLoad = {},
          head = $('head:first'),
          cssAdded = false;

      if (codes.length) {
        // Create a RegExp for matching the class of the code with any brush.
        // The code should have a class of `brush-TYPE` ex. `brush-JScript`.
        // Matching is case sensitive.
        classesRx = new RegExp('(?:^|\\s)brush-(' + brushes.join('|') + ')(?:$|\\s)');

        // Filter the codes if there are any of them, that should be syntax highlighted.
        codes = codes.filter(function () {
          var matches = this.className.match(classesRx),
              lineMatches,
              brush;
          // Check if there are any classes we have as a brush
          if (matches) {
            // Store the brush in the items to load, and as data in the element.
            brush = matches[1];
            toLoad[brush] = brush;
            $.data(this, 'brush', brush);

            lineMatches = this.className.match(/(?:^|\s)brushline-(\d+)(?:$|\s)/);
            if (lineMatches) {
              $.data(this, 'brushFirstLine', lineMatches[1]);
            }

            return true;
          }
          else {
            return false;
          }
        });

        // Check if we still have elements...
        if (codes.length) {
          // Load the CSS for the highlighter.
          $('<link>').attr({
            'rel'  : 'stylesheet',
            'type' : 'text/css',
            'href' : settings.path + '/styles/shCore.css',
            'media': 'all'
          }).appendTo(head);
          $('<link>').attr({
            'rel'  : 'stylesheet',
            'type' : 'text/css',
            'href' : settings.path + '/styles/shThemeDefault.css',
            'media': 'all'
          }).appendTo(head);

          // Load the core for the highlighter.
          $.ajax({
            'url': settings.path + '/scripts/shCore.js',
            'cache': true,
            'dataType': 'script',
            'success': function () {
              var m,
                  itemsLeft,    // # of scripts that are left to be loaded
                  sl,           // # of scripts
                  scripts = [], // Array of script URLs.
                  i = 0;

              // List all script to load.
              for (m in toLoad) {
                if (toLoad.hasOwnProperty(m)) {
                  scripts.push(settings.path + '/scripts/shBrush'+ m +'.js');
                }
              }
              sl = itemsLeft = scripts.length;
              // Load the scripts.
              for (; i < sl; i += 1) {
                 $.ajax({
                  'url': scripts[i],
                  'cache': true,
                  'dataType': 'script',
                  'complete': function () {
                    itemsLeft -= 1;
                    // Check if all the scripts are loaded.
                    if (!itemsLeft) {
                      // All loaded, so apply the highlighter.
                      codes.each(function () {
                        // Get rid of all the BR tags.
                        var el = $(this).find('br').replaceWith("\n");

                        // Finally highlight with some defaults,
                        // using the brush stored in the elements' data.
                        SyntaxHighlighter.highlight({
                          'brush'      : $.data(this, 'brush').toLowerCase(),
                          'tab-size'   : 2,
                          'toolbar'    : false,
                          'first-line' : $.data(this, 'brushFirstLine') || 1
                        }, this);
                      });
                    }
                  }
                });
              }
            }
          });
        }
      }
    };

    // Run init function when the DOM is ready.
    $(init);
  })($, window, document);
}

Hogyan működik

Vizsgáljuk meg kicsit a kódot. Az eleje az Űrlap mező helykitöltő jQuery kiegészítő-ben említett mintára épül, így az ott olvasottak itt is állnak.

// Global Killswitch
if (Drupal.jsEnabled) {
  (function ($, window, document, undefined) {
    //…
  })($, window, document);
}

Azután következik a támogatott “ecsetek” listája, vagyis ezeket a nyelveket tudjuk színezni, és ilyen néven kell őket megadni a CSS osztályban, hogy a hozzá kapcsolódó JavaScript kód betöltődjön:

var brushes = [
  "AppleScript",
  "AS3",
  "Bash",
  "ColdFusion",
  "Cpp",
  "CSharp",
  "Css",
  "Delphi",
  "Diff",
  "Erlang",
  "Groovy",
  "Java",
  "JavaFX",
  "JScript",
  "Perl",
  "Php",
  "Plain",
  "PowerShell",
  "Python",
  "Ruby",
  "Sass",
  "Scala",
  "Sql",
  "Vb",
  "Xml"
],

Az init függvény az általános inicializációt végzi. Pár változóra szükségünk lesz a kódban, ezért ezeket gyorsan előkészítjük, ugyan a bonyolultabbakkal kicsit várunk, hogy feleslegesen ne terheljük a gépet, amennyiben nincs is mit csinálni.

init = function () {
  var settings = Drupal.settings.syntax_highlighter,
      bl = brushes.length,
      classesRx,
      i = 0,
      codes = $(settings.selector),
      toLoad = {},
      head = $('head:first'),
      cssAdded = false;

Osztály vizsgálat

Most már tudjuk mik a támogatott osztályok, és hogy milyen elemeket kell vizsgálni, akkor nézzük meg, rendelkeznek-e ezek a DOM elemek a kívánt osztállyal. Az elem osztálya a következően kell, hogy kinézzen: brush-Ecset , ahol az Ecset egy az előbb említett elemek közül a listában, például Php esetén az elem (egyik) osztálya brush-Php kell, hogy legyen. Ennek ellenőrzéséhez egy reguláris kifejezést építünk.

if (codes.length) {
  // Create a RegExp for matching the class of the code with any brush.
  // The code should have a class of `brush-TYPE` ex. `brush-JScript`.
  // Matching is case sensitive.
  classesRx = new RegExp('(?:^|\\s)brush-(' + brushes.join('|') + ')(?:$|\\s)');

  //…
}

Az előbb elkészített reguláris kifejezést pedig alkalmazzuk az elemekre, ezáltal kiszűrjük a nekünk kellőket:

// Filter the codes if there are any of them, that should be syntax highlighted.
codes = codes.filter(function () {
  var matches = this.className.match(classesRx),
      lineMatches,
      brush;
  // Check if there are any classes we have as a brush
  if (matches) {
    // Store the brush in the items to load, and as data in the element.
    brush = matches[1];
    toLoad[brush] = brush;
    $.data(this, 'brush', brush);

    lineMatches = this.className.match(/(?:^|\s)brushline-(\d+)(?:$|\s)/);
    if (lineMatches) {
      $.data(this, 'brushFirstLine', lineMatches[1]);
    }

    return true;
  }
  else {
    return false;
  }
});

Amennyiben a jQuery filter függvényének egy függvényt adunk át paraméterként, akkor ezt a függvényt fogja meghívni minden egyes kiválasztott elemre, és a this mindig az aktuális elemre fog mutatni. Amennyiben ez a függvény true értékkel tér vissza, akkor az elem benne marad a kiválasztásban, ha false -szal, akkor kikerül belőle.
Ha az elem osztályár illik a reguláris kifejezés, megvizsgáljuk, hogy esetleg meg volt-e adva sorszám is, ezzel tudjuk jelölni, hogy az idézett kódrészlet hányadik sornál kezdődik. Ezt például a brushline-123 osztály a kód blokkhoz való hozzáadásával tudjuk megtenni. Amennyiben meg volt adva sorszám, akkor azt eltároljuk ezt az ecsettel egyetemben a jQuery data függvényével hozzákapcsolva az adatot a DOM elemhez.

Betöltés

Ha maradt színezni való elemünk, akkor betöltjük az alapvető CSS fájlokat, és a fő JavaScript fájlt, amitől a színező ecsetei függenek.

// Load the CSS for the highlighter.
$('<link>').attr({
  'rel'  : 'stylesheet',
  'type' : 'text/css',
  'href' : settings.path + '/styles/shCore.css',
  'media': 'all'
}).appendTo(head);
$('<link>').attr({
  'rel'  : 'stylesheet',
  'type' : 'text/css',
  'href' : settings.path + '/styles/shThemeDefault.css',
  'media': 'all'
}).appendTo(head);

// Load the core for the highlighter.
$.ajax({
  'url': settings.path + '/scripts/shCore.js',
  'cache': true,
  'dataType': 'script',
  'success': function () {
    var m,
        itemsLeft,    // # of scripts that are left to be loaded
        sl,           // # of scripts
        scripts = [], // Array of script URLs.
        i = 0;

    // List all script to load.
    for (m in toLoad) {
      if (toLoad.hasOwnProperty(m)) {
        scripts.push(settings.path + '/scripts/shBrush'+ m +'.js');
      }
    }
    sl = itemsLeft = scripts.length;
    // Load the scripts.
    for (; i < sl; i += 1) {
      //…
    }
  }
});

A CSS fájlokat egyszerű betölteni, egyszerűen a <link> elemeket hozzá kell adni a <head> -hez. A JavaScript kód betöltése kicsit trükkösebb, ugyanis, mint írtam az ecsetek függenek a fő modultól, azaz előbb azt kell betölteni, utána lehet csak az ecseteket. Erre használjuk a $.ajax success eseménykezelőjét. Ez a függvény akkor hívódik meg, mikor a $.ajax -nek megadott JavaScript fájl már be van töltve, és már le is futott. Az eseménykezelőben összegyűjtjük a betöltendő JavaScripteket, majd betöltjük őket a második for ciklusban.

$.ajax({
 'url': scripts[i],
 'cache': true,
 'dataType': 'script',
 'complete': function () {
   itemsLeft -= 1;
   // Check if all the scripts are loaded.
   if (!itemsLeft) {
     // All loaded, so apply the highlighter.
     codes.each(function () {
       // Get rid of all the BR tags.
       var el = $(this).find('br').replaceWith("\n");

       // Finally highlight with some defaults,
       // using the brush stored in the elements` data.
       SyntaxHighlighter.highlight({
         'brush'      : $.data(this, 'brush').toLowerCase(),
         'tab-size'   : 2,
         'toolbar'    : false,
         'first-line' : $.data(this, 'brushFirstLine') || 1
       }, this);
     });
   }
 }
});

Ezeknek az ecseteknek a betöltődése esetén vizsgáljuk hogy mindegyik be lett-e már töltve, és ha igen, akkor alkalmazzuk a már szűrt kód blokkokra a kód színezőt. Mivel a Code Filter modul <br> elemeket rak a sortörések helyére, és a SyntaxHighlighter ezt nem támogatja ezért ezeket vissza kell alakítani sortöréssé. A kódszínezés beállításai a Drupal szabályait követik, egyedül az eszközsort kapcsoltam ki, amíg nem tudom szépen bekonfigurálni.

comments powered by Disqus