Client-seitiges Syntax-Highlighting

  5. November 2018 - Guido Flohr

Eine häufige Anforderung besteht darin, Syntax-Hervorhebung auf Code-Blöcke anzuwenden. Für Qgoda wird empfohlen, Blöcke mit Quelltext lediglich semantisch korrekt zu markieren, und das Highlighting dem Client, also dem Browser beim Rendern der generierten Seiten zu überlassen..

Wie werden Code-Blöcke markiert?

Code-Blöcke sollten in ein <code>-Element eingeschlossen werden, dass seinerseits in einem <pre>-Element steckt. Die jeweilige Programmiersprache des Code-Blockes sollte als CSS-Klasse im Format language-PROGRAMMIERSPRACHE angegeben werden:

&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (options.debug) {
    console.log("Options: ", options);
}&lt;/code&gt;&lt;/pre&gt;

Die umschließenden <pre>- und <code>-Tags sollten in die gleiche Zeile wie der folgende Code gesetzt werden, damit es keine hässlichen Leerzeilen am Anfang und Ende des Code-Blockes gibt.

Erzeugung Semantisch korrekter Code-Blöcke

Manuelle Erzeugung semantisch korrekter Code-Blöcke

Weil (fast) alles an Markup den Markdown-Prozessor unverändert passiert, kann man natürlich die Code-Blöcke einfach von Hand als HTML eingeben, so wie gerade gezeigt:

&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;if (options.debug) {
    console.log("Options: ", options);
}&lt;/code&gt;&lt;/pre&gt;

“Fenced” Code-Blocks

So-genannte fenced, also “umzäunte” Code-Blöcke werden von den meisten Markdown-Prozessoren unterstützt. Solche Code-Blöcke werden von dreifachen Backticks ``` umschlossen:

&#x60;&#x60;&#x60;
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
&#x60;&#x60;&#x60;

Dies erzeugt exakt das Konstrukt, das wir benötigen, nämlich ein <code>-Element innerhalb eines <pre>-Elementes. Allerdings hat keines der Tags irgendwelche Attribute. Genauer gesagt fehlt das Class-Attribut class="language-javascript". Man kann aber die Sprache einfach nach den öffnenden Backticks angeben:

&#x60;&#x60;&#x60;javascript
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
&#x60;&#x60;&#x60;

Es gibt im Moment allerdings noch ein kleines Problem damit. Der Default-Markdown-Prozessor von Qgoda ist noch Text::Markdown, das Fenced Code-Blocks mit Sprachangabe nicht unterstützt. Stattdessen interpretiert es die Sprachangabe als Teil des Codes. Diese Syntax lässt sich deshalb nur verwenden, wenn Text::Markdown::Hoedown als Markdown-Prozessor verwendet wird.

Syntax-Highlighting mit PrismJS

PrismJS ist ein populärer Syntax-Highlighter, der vollständig in JavaScript geschrieben ist und eine beeindruckende Anzahl an Sprachen unterstützt.

Allgemeine Verwendung

Die minimal benötigen JavaScript- und CSS-Dateien lassen sich dem folgenden Code-Schnipsel entnehmen:

&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link href=&quot;/assets/css/prismjs/themes/prism.css&quot; rel=&quot;stylesheet&quot; /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script href=&quot;/assets/js/prismjs/prism.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;

Das Standard-Stylesheet wird in Zeile 4 eingebunden, und die Core-Bibliothek in Zeile 7. Es muss natürlich sichergestellt sein, dass sie an den entsprechenden Stellen auch gefunden werden.

Verwendung eines Themes für PrismJS

Mit PrismJS wird eine Reihe von Themes ausgeliefert. Das Default-Theme lässt sich überschreiben, indem eine weitere Stylesheet-Datei eingebunden wird:

&lt;link href=&quot;/assets/css/prismjs/themes/prism.css&quot; rel=&quot;stylesheet&quot; /&gt;
&lt;link href=&quot;/assets/css/prismjs/themes/prism-coy.css&quot; rel=&quot;stylesheet&quot; /&gt;

So lässt sich der Stil auf das “Coy”-Theme umstellen.

Laden von Highlightern und Plug-Ins für PrismJS

Um Speicher und Bandbreite zu sparen, lädt PrismJS nicht alle Highlighter-Komponenten und Plug-Ins automatisch. Sie müssen vielmehr explizit angegeben werden.

Das untenstehende, vollstdändige Beispiel würde Highlighting für die Sprache JavaScript und das Plug-In line-numbers aktivieren. Mit Hilfe dieses Plug-ins wird mit CSS allen Zeilen die Zeilennummer vorangestellt:

&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;link href=&quot;/assets/css/prismjs/themes/prism.css&quot; rel=&quot;stylesheet&quot; /&gt;
    &lt;link href=&quot;/assets/css/prismjs/themes/prism-coy.css&quot; rel=&quot;stylesheet&quot; /&gt;
    &lt;link href=&quot;/assets/css/prismjs/plugins/line-numbers/prism-line-numbers.css&quot; rel=&quot;stylesheet&quot; /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script href=&quot;/assets/js/prismjs/prism.js&quot;&gt;&lt;/script&gt;
    &lt;script href=&quot;/assets/js/prismjs/plugins/line-numbers/prism-line-numbers&quot;&gt;&lt;/script&gt;
    &lt;script href=&quot;/assets/js/prismjs/components/prism-javascript.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;

Um Unterstützung für weitere Sprachen zu aktivieren, müssen nach Zeile 11 lediglich die entsprechenden weiteren PrismJS-Komponenten hinzugefügt werden.

Verwendung des Syntax-Highlighter-Plug-Ins von Qgoda

Fenced Code-Blöcke sind nicht immer ausreichend:

  1. Der Markdown-Prozessor könnte sie nicht unterstützen, oder keine Sprachangaben erlabuen.
  2. Oft müssen weitere Attribute von HTML-Elementen, insbesondere CSS-Klassen gesetzt werden.

Installation

Das Qgoda-Highlighter-Plug-in wird wie jedes andere Plug-in installiert, normalerweise also so:

$ cd /path/to/your/project
$ npm install gflohr/qgoda-plugin-tt2-highlight

Mit yarn statt npm lautet das Kommando yarn add gflohr/qgoda-plugin-tt2-highlight.

Verwendung

Jetzt wird die Syntax-Hervorhebung mit Direktiven für Template Toolkit aktiviert:

[% USE Highlight %]
[% FILTER $Highlight &quot;language-javascript&quot; &quot;line-numbers&quot;
                     &quot;data-start&quot;=42 %]
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
[% END %]

Alle positionellen Argument für das Fitler-Plug-In (Zeile 2) werden der CSS-Klasse des umgebenden <pre>-Elements zugefügt. Die benannten Argument dagegen werden in HTML-Attribute und ihre entsprechenden Werte umgewandelt.

Das obige Beispiel erzeugt den folgenden HTML-Code:

&lt;pre class=&quot;language-html line-numbers&quot; data-start=&quot;5&quot;&gt;if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}&lt;/code&gt;&lt;/pre&gt;

Provided that you have correctly loaded the PrismJS line-numbers plug-in, this will highlight the code as JavaScript in the browser, add line numbers in front of every line and start the line numbering with line 5. For instance, the code examples on this very page are produced with directives like this:

Vorgaben für die ganze Seite

Es ist nicht unwahrscheinlich, dass sämtliche Code-Beispiele auf einer Seite in der gleichen Programmiersprache geschrieben sind, und deshalb die gleichen Einstellungen verwenden sollte. Deshalb kann man auch globale, für die ganze Seite geltende Einstellungen direkt in der USE-Direktive vergeben, mit der der Filter geladen wird.

[% USE Highlight &quot;language-javascript&quot; &quot;line-numbers&quot; %]
...
[% FILTER $Highlight %]
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
...
[% END %]

Alle FILTER-Aufrufe teilen nun die gleichen Einstellungen. Genauer gesagt erhalten sie Die Summe aller Argumente, die für USE und für FILETR übergeben wurden.

Um eine bestimmte CSS-Klasse für einen einzelnen FILTER zu deaktivieren, übergibt man diesen Klassennamen mit vorangestelltem Minuszeichen (-):

...
[% FILTER $Highlight &quot;-language-javascript&quot; &quot;language-html&quot; %]
&lt;link href=&quot;styles.css&quot; rel=&quot;stylesheet&quot;>
...
[% END %]

Dieser Codeblock wird nun ausnahmsweise nicht als JavaScript, sondern als HTML hervorgehobe.

Alternative: Ein JavaScript-Hack

Die Verwendung des Qgoda-Syntax-Highlighter-Plug-ins ist sehr flexibel, bedeutet aber auch eine Menge Tipparbeit im Vergleich zu fenced Code-Blocks. Auf dieser Site verwenden wir stattdessen in der Regel einen kleinen JavaScript-Hack, der es erlaubt, das Plug-In `line-numbers" direkt mit fenced Code-Blocks zu aktivieren:

&#x60;&#x60;&#x60;javascript;line-numbers
if (options.debug) {
    console.log(&quot;Options: &quot;, options);
}
&#x60;&#x60;&#x60;

Siehe den entsprechenden JavaScript-Quelltext für Einzelheiten!

Stolpersteine

Es gibt zwei kleine Problemchen, die man beachten muss.

Code-Blöcke ohne Sprachangabe

Benutzt man fenced Code-Blocks oder das Qgoda-Plug-In für Syntax-Hervorhebung ohne Angabe einer Sprache, wird der entsprechende Block von PrismJS ignoriert. Das wird in einer späteren Version des Plug-Ins behoben werden (siehe https://github.com/gflohr/qgoda-plugin-tt2-highlight/issues/1 und https://github.com/gflohr/qgoda/issues/51). Bis dahin muss man entweder jeden betreffenden Code-Block mit der Sprache “None” oder mit der CSS-Klasse “language-none” markieren, oder wieder etwas JavaScript bemühen:

var codes = document.querySelectorAll('pre>code');
for (var i = 0; i < codes.length; ++i) {
    var parent = codes[i].parentElement;
    if (!parent.hasAttribute('class'))
        parent.setAttribute('class', 'language-none');
}

Man können meinen, dass die Prüfung in Zeile 4 pre-Elemente ignoriert, die ein Class-Attribut haben, aber keine Angabe von language-*. Das kann allerdings nicht passieren, weil alle Blöcke generiert sind, und sie eben kein Class-Attribut haben.

Gut, das ist nicht 100 % wahr. Hat man solch einen Block mit dem Highlighter-Plug-In erstellt, könnte es ein Class-Attribut aber keine Sprachangabe haben. Aber das ist deine eigene Schuld, und es ist offensichtlich, wie man das Problem behebt.

Gleichzeitige Verwendung von css-modules und line-numbers

Das andere Problem ist etwas esoterisch und wird durch einen kleine Mangel im Plug-In-System von PrismJS verursacht. Das Plug-In css-modules dient dazu, automatisch Namensräume für CSS-Klassen und ID-Attribute nach BEM-Methodologie zu verwenden.

Das Plug-In line-numbers dagegen ignoriert BEM. Es sucht nach Code-Blöcken, die exakt die Klasse line-numbers haben. Die Lösung des Problems besteht darin, einfach beide Klassen zu verwenden, also sowohl mit BEM-Präfix zum Stylen als auch die Variante ohne Präfix für den JavaScript-Code des Plug-Ins.

Wie das genau funktioniert, kann man dem englischen Quelltext dieser Seite entnehmen.

Der BEM-Klassenname wird in der Variablen css.prism.line_numbers gespeichert. Der Hash, der die Variable enthält wird in https://github.com/gflohr/qgoda-site/blob/master/_views/functions/css-modules.tt gelesen, und diese Funktion wird oben im englischen Quelltext dieser Seite eingebunden..