Erweiterungen für Asciidoctor erstellen

Wer technische Dokumentationen erstellen muss, hat mit Asciidoctor eine einfache aber leistungsfähige Lösung zur Hand. Asciidoctor ist ein Textprozessor, der Vorlagen aus einfachen Textdateien in HTML, PDF, EPUB3 oder Docbook Format umwandelt. Dabei orientiert sich der Befehlssatz des Textprozessor grundsätzlich an MarkDown. Neben den üblichen Vorteilen von MarkDown (einfach zu erlernen, schnell zu bedienen, kein spezieller Editor notwendig, keine Lizenzkosten) bietet Asciidoctor noch weitere Funktionalitäten.

  • Es können Dateien inkludiert werden und z.B. als Quellcode angezeigt werden
  • Quellcode kann mit Syntax Highlighting dargestellt werden und der Highlighter ist als Plugin austauschbar
  • Diagramme können eingefügt werden. Über Plugins sind z.B. PlantUML, GnuPlot oder GraphViz verwendbar.
  • Die Darstellung von Mathematische Gleichungen wird unterstützt
  • Inhaltsverzeichnis, Literaturverzeichnis und Index werden automatisch erzeugt
  • Schnittstellen für Erweiterungen

Der letzte Punkt ist für Software Entwickler immer von Interesse, denn auch wenn es keine Anforderungen gibt, mach sich doch bei der Aussicht auf Ergänzungen eine gewisse Vorfreude breit.

Die Asciidoctor API stellt acht Schnittstellen bereit, an denen in die Arbeit des Textprozessors eigegriffen werden kann.

  1. Den Include Processor für eigene include Anweisungen
  2. Den Preprocessor zum Manipulieren der Eingabe
  3. Block Macro Processor zur Verarbeitung eigener Block Makros
  4. Block Processor um eigene Blöcke zu verarbeiten
  5. Treeprocessor zur Manipulation des AST nach dem Parsen
  6. Inline Macro Processor zur Verarbeitung eigener Inline Makros
  7. Postprocessor zur Manipulation der Zieldokumentes
  8. DocInfoProcessor zur Manipulation von Metadaten des Zieldokumentes

Um die Freude der Java Entwickler einen Moment zu trüben, sollte erwähnt werden, dass Asciidoctor in Python geschrieben ist. Glücklicherweise existiert das Projekt AsciidoctorJ, mit dem Asciidoctor Anwendungen auf einer JVM gestartet werden können. Das Project stellt eine Java API zur Asciidoctor zur Verfügung, über die Java Implementierung für die Schnittstellen angebunden werden können.

Für einen ersten kurzen Einblick in die Möglichkeiten der API wird ein Inline Macro implementiert. Es stellt Befehle bereit, um den Namen eines Feiertages (in NRW) auszugeben.

Ein Inline Macro hat immer den Aufbau name:target[attributes]. Dabei ist name der Name des Makros, target ist das Parameter des Aufrufes und attributes sind optionale Attribute, um das Macro weiter zu parametrisieren.

Für die Ausgabe des Feiertages werden drei Varianten implementiert holiday:before[], holiday:at[] und holiday:after[]. Das erste Macro gibt den letzten Feiertag vor dem Datum aus, das zweite Macro den Feiertag an dem Datum und das letzte Macro gibt den ersten Feiertag nach dem Datum aus. Bei dem Datum handelt es sich immer um das aktuelle Datum, außer über das Attribute date="yyyy-mm-dd" wurde ein spezielles Datum ausgewählt.

Damit ein Inline Macro Processor erkannt wird, muss er die Klasse InlineMacroProcessor erweitern und mit der @Name annotiert sein.

@Name("holiday")
public class HolidayInlineMacroProcessor extends InlineMacroProcessor {

  @Override
  public Object process(ContentNode parent, String target, Map<String, Object> attributes) {
    // ...
  }
}

Das Macro soll holiday heißen, daher die Annotation @Name("holiday").

Die zu implementierende Methode process dient zur Verarbeitung des Makros und wird mit dem Elterknoten im Dokument, dem Macro Parameter und den optionalen Attributen parametrisiert.

Innerhalb der Methode wird zuerst das Datum bestimmt, für das der Feiertag gesucht wird und dann eine Holidays Instanz für NRW bestimmt. Die Holidays Klasse stammt aus dem Projekt Holidays, das in der Reihe Kalenderspielereien mit Java realisiert wurde. In der Methode getHolidayLabel wird der Name des Feiertags bestimmt und damit ein PhraseNode als Rückgabewert der Methode erzeugt.

@Override
public Object process(ContentNode parent, String target, Map<String, Object> attributes) {
  Object dateAttribute = attributes.get("date");
  try {
    LocalDate date = getDate(dateAttribute);
    Holidays holidays = Holidays.in(GermanFederalState.NW);
    Supplier<String> elseSupplier = () -> Objects.toString(attributes.get("else"), "");

    return createPhraseNode(parent, CONTEXT, getHolidayLabel(target, date, holidays, elseSupplier), attributes);
  } catch (DateTimeParseException e) {
    log(new LogRecord(Severity.WARN, "could not parse: " + dateAttribute));
    return createPhraseNode(parent, CONTEXT, "FEHLER", attributes);
  } catch (RuntimeException e) {
    log(new LogRecord(Severity.WARN, "exception: " + e));
    return createPhraseNode(parent, CONTEXT, "FEHLER", attributes);
  }
}

Um den eigenen Processor auszuprobieren kann er über das extension Element des asciidoctor-maven-plugin eingebunden werden.

<plugin>
  <groupId>org.asciidoctor</groupId>
  <artifactId>asciidoctor-maven-plugin</artifactId>
  <version>2.1.0</version>
  <dependencies>
    <dependency>
      <groupId>org.asciidoctor</groupId>
      <artifactId>asciidoctorj-pdf</artifactId>
      <version>1.5.3</version>
    </dependency>
    <dependency>
      <groupId>de.schegge</groupId>
      <artifactId>asciidoctor-plugins</artifactId>
      <version>0.1.0</version>
    </dependency>
  </dependencies>
  <executions>
    <execution>
      <id>output-pdf</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>process-asciidoc</goal>
      </goals>
      <configuration>
        <backend>pdf</backend>
        <doctype>book</doctype>
        <sourceDirectory>${basedir}/src/main/asciidoc</sourceDirectory>
        <sourceDocumentName>index.adoc</sourceDocumentName>
        <extensions>
          <extension>
            <className>de.schegge.asciidoctor.HolidayInlineMacroProcessor</className>
            <blockName>holiday</blockName>
          </extension>
       </extensions>
     </configuration>
    </execution>
  </executions>
</plugin>

Angewendet auf den Eingangstext auf der linken Seite produziert der HolidayInlineMacroProcessor die Ausgabe auf der rechten Seite.

Heute ist holiday:at[else="kein Feiertag"].

Der 11. Juni ist holiday:at[date="2020-06-11"].

Der nächste Feiertag nach dem 2020-05-30 ist holiday:after[date="2020-08-30"].

Der nächste Feiertag nach dem 2020-02-23 ist holiday:after[date="2020-02-23"].

Der letzte Feiertag vor dem 2020-08-30 ist holiday:before[date="2020-08-30"].

Der letzte Feiertag vor dem 2020-02-23 ist holiday:before[date="2020-02-23"].
Heute ist kein Feiertag.

Der 11. Juni ist Fronleichnam.

Der nächste Feiertag nach dem 2020-05-30 ist Tag der Deutschen Einheit.

Der nächste Feiertag nach dem 2020-02-23 ist Karfreitag.

Der letzte Feiertag vor dem 2020-08-30 ist Fronleichnam.

Der letzte Feiertag vor dem 2020-02-23 ist Neujahr.

Obwohl die Möglichkeiten von Asciidoctor für die technische Dokumentation kaum Wünsche übrig lassen, können mit der Asciidoctor API interessante Erweiterungen im eigenen Projektumfeld erstellt werden.