namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


anwenden:onesource:dokgen

dokgen : ein Dokumentationsgenerator für C++

Stimmen Quelltext und Kommentar nicht überein,
sind vermutlich beide falsch.
— Sprichwort

Version: 2011-04-29. Projektstatus

Überblick

Quelltext liegt gewöhnlich als "plain-text" vor. Dokumentationen sollen dagegen "pretty printed" sein. Die Druckaufbereitung kann aufwendig sein, andere Dateiformate erfordern und führt zu gedruckten Dokumenten, die nicht unbedingt den aktuellen Stand wiedergeben. Liegen Quelltext und Dokumentation getrennt vor, werden sie sich später oder (eher) früher auseinander entwickeln. Jedes Stück Wissen sollte nur an einer Stelle gespeichert sein. Ideal wäre es, die Dokumentation parallel zur Quelltext-Entwicklung zu schreiben. Und weil Umschalten nervt, sollten beide dicht nebeneinander in einer Datei stehen. Ändert sich etwas, werden Quelle und Dokumentation zusammen überarbeitet.

Die Idee ist nicht neu. Donald E. Knuth's Literate Programming-System WEB leistet so etwas für TeX. Das XML-Format DocBook entwickelt sich, wenn auch langsam, zu einem Standard für elektronische Dokumentation und Buchdruck. Java-Programmierer dokumentieren ihre Quellen mit dem zum System gehörenden Standardwerkzeug javadoc. Für C++ gibt es Doxygen und mittlerweile nicht mehr aktive Projekte wie ccdoc und DOC++. Die verschiedenen Systeme verfolgen dabei aber unterschiedliche Ziele. Literate Programming übersetzt und dokumentiert top-down entworfene Programme, die TeX-Syntax ist nicht ohne weiteres geläufig. DocBook kann als Ausgangspunkt zur Erzeugung unterschiedlicher Dateien dienen. Wie bei WEB geht aber der direkte Bezug zum Quellcode verloren. Doxygen dokumentiert Entwickler-Schnittstellen von Klassen und Methoden eines Systems wahlweise in LaTeX und HTML, eignet sich aber weniger zum Entwickeln einer Abhandlung. Alle Systeme erfordern Zusatzwerkzeuge (Pascal WEB oder CWEB, XSL-Transformationen für DocBook etc.) oder sind mit solchen einfacher nutzbar (spezielle Editoren, "Quellcode"-Vervollständigung).

Was mir fehlt(e), ist ein System, mit dem Programmquellen für die Lehre mit Erläuterungen versehen, getested und schnell als Webseiten aufbereitet werden können. Es geht um ein System mit einfacher Syntax, einfacher Handhabung und dem Prinzip "one source". Die Kombination von C++-Kommentar und Wiki-artiger Syntax erscheint mir als gangbarer Mittelweg. dokgen ist nicht der Stein der Weisen und nicht die Lösung aller Probleme. Es kollidiert mit der Syntax von Doxygen (3 Schrägstriche). Vor allem aber ist Dokumentation nicht kostenlos zu haben. Auch die von dokgen darzustellende Information muss schon in der Ausgangsdatei enthalten sein. Dokumentations- und Quelltextabschnitte mehrerer zusammengehöriger Quelltexte bilden gemeinsam eine einzige zu pflegende Datei eines Moduls oder Programms:

Mit dokgen werden aktuelle Dokumente aus den Programmquellen erzeugt. Daneben können andere Programme den Quelltext für den Übersetzungsprozess aufbereiten. Mit dem Werkzeug make lassen sich solche Vorgänge auch automatisieren. Deutsche Umlaute ÄÖÜäöüß werden mit dem Zeichensatz ISO 8859-1 (ISO Latin-1) übersetzt. Der Aufruf des Konsolen-Programms erfordert die Umlenkung von Ein- und Ausgabe. Die Anwendung des Programms auf sein eigenes Quelldokument

dokgen < dokgen.txt > dokgen.htm

erzeugte die aktuell betrachtete Datei.

Dokumentation und Quelltext aus einer Quelle

Die Erläuterungen sind im Quelltext integriert. Dokumentationszeilen beginnen mit genau drei Schrägstrichen am linken Rand:

 /// @title dokgen : ein Dokumentationsgenerator
 /// @author René Richter
 /// @keywords Dokumentation, automatisiert
 /// > Stimmen Quelltext und Kommentar nicht überein,
 /// > sind vermutlich beide falsch.
 /// >--- Sprichwort
 /// Version: 2002-11-25. @ref Projektstatus 
 /// Kurz-Dokumentation: @link dokgen0.htm
 /// === Überblick
 /// Liegen Quelltext und Dokumentation getrennt vor,
 /// werden sie sich später oder (eher) früher auseinander entwickeln.

(Um dies zu zeigen, wurde der Anfang dieses Dokumentes um ein Zeichen eingerückt.) Quelltext sind dagegen "normale" Zeilen:

// bei Übersetzung aus fortlaufendem Quelltext (ohne extract-Prozess)
#define ALL_IN_ONE

Kommentare : und ~ am Zeilenanfang von Quelltextabschnitten

 //: dateiname   // Dateianfang
                 // Dateiinhalt
 //~             // Dateiende

sind vorgesehen, um mit

extract < dokgen.txt

die Quelltexte einzelner Module aus der Originaldatei herausziehen zu können. Dann ist das Makro ALL_IN_ONE unbekannt und die Header müssen eingebunden werden. Die Programme extract und dokstrip (siehe Anhang) sollen später das System ergänzen.

Syntax

Dokumentkopf

Jedes Dokument kann mit den Angaben

@title Überschrift
@author Autor (nur am Dokumentanfang)
@keywords Suchbegriffe (nur am Dokumentanfang)

beginnen. Die Einträge gehen bis zum Zeilenende. Mehrzeilige Einträge sind in einen geschweiften Klammerblock einzuschließen. Alle Angaben sind optional. Sobald alle Einträge festgelegt sind, eine Leerzeile folgt oder normaler Text folgt, ist der Dokumentkopf abgeschlossen.

Überschriften

Lange Texte erfordern eine Gliederung. Es erfolgt keine automatische Nummerierung.

=== 1 Kapitel
==== 1.1 Unterkapitel
===== 1.1.1 Abschnitt
====== 1.1.1.1 Unterabschnitt
 /// === 1 Kapitel
 /// ==== 1.1 Unterkapitel
 /// ===== 1.1.1 Abschnitt
 /// ====== 1.1.1.1 Unterabschnitt

Textelemente

Der Dokumentationstext ist strukturierbar mit

  • Listen
    1. ohne Nummern,
    2. mit Nummern,
    3. von Begriffsdefinitionen, z.B.
      • Begriff : erläuternder Text
  • Tabellen,
  • eingerücktem Text und
  • Zitaten.

Textelemente beginnen mit Marken in der fünften Spalte:

| Tabelle.
: Eingerückter Text. Jeder Doppelpunkt ist eine Ebene.
> Zitat. Die Herkunft wird durch >— markiert.
* Liste.
# Nummerierte Liste.
; Definitionsliste.

Listen beliebigen Typs sind schachtelbar:

 /// * Listen
 /// *# ohne Nummern,
 /// *# mit Nummern,
 /// *# von Begriffsdefinitionen, z.B.
 /// *#; Begriff : erläuternder Text

Mehrzeilige Listeneinträge sind in einen Klammerblock einzuschließen.

Tabellenzellen beginnen immer mit einem senkrechten Strich. Optional kann dem Strich ein Ausrichtungspfeil folgen.

|< linksbündig
|^ zentriert
|> rechtsbündig
ohne Pfeil Standardausrichtung
 /// |< linksbündig  
 /// |^ zentriert 
 /// |> rechtsbündig 
 /// | Standardausrichtung

Eingerückter Text eignet sich besonders für Algorithmenbeschreibungen (Pseudocode):

für alle Dokumentzeilen
verarbeiten
 /// : für alle Dokumentzeilen
 /// :: verarbeiten

Zitate beginnen mit einem Zeichen > (wie in Mailsystemen). Die Herkunft kann nach >— angegeben werden:

 /// > Suddenly, documentation isn't so bad.
 /// >--- Andrew Hunt, David Thomas: The Pragmatic Programmer
Suddenly, documentation isn't so bad.
— Andrew Hunt, David Thomas: The Pragmatic Programmer

Marken im Fließtext

Im Dokumentabschnitten können Marken eingefügt werden.

@code Schlüsselwort im Text
@image Bild
@label Verweisziel innerhalb des Dokuments
@link Verweis außerhalb des Dokuments (Hypertextreferenz)
@ref Verweis innerhalb des Dokuments

In @code{…}-Abschnitten dürfen keine geschweiften Klammern vorkommen. Es kann ein beliebiges anderes Zeichen als Begrenzer eingesetzt werden, das nicht innerhalb des Blocks vorkommt: @code|{…}| erzeugt {…}.

Bild- und Verweismarken folgt eine Bezeichnung ohne Leerzeichen. Optional kann ein geschweifter Klammerblock mit einem Text folgen (Bildunterschrift oder Beschreibung des Verweiszieles).

 /// Siehe @ref Anhang {Anhang und Projektstatus}.

Siehe Anhang und Projektstatus.

Implementierung des dokgen-Systems

Das Programm dokgen besteht aus mehreren Klassen:

  • ParseError zur Beschreibung von Fehlern bei der Verarbeitung,
  • Lexer zur lexikalischen Analyse,
  • Parser führt die syntaktische Analyse durch und schickt die Inhalte zum Ausgabeziel.
  • Target ist ein einfaches Ausgabeziel zur Demonstration und zum Debuggen.
  • HTMLTarget generiert ein HTML-Dokument.
  • DokuWikiTarget erzeugt einen DokuWiki-Quelltext.

Weitere Ziele (Ausgabeformate) können aus der Target-Klasse abgeleitet werden. Dazu muss das Hauptprogramm angepasst werden.

Fehlerzustand

//: parseerr.hpp : dokgen Fehlermeldungen - R.Richter 2002-11-18
/////////////////////////////////////////////////////////////////////
 
#ifndef PARSEERR_HPP
#define PARSEERR_HPP
 
#include <string>
#include <sstream>
 
class ParseError
{
public:   
  ParseError(std::string why, int row, int column, std::string line)
  {
    std::ostringstream out;
    out << "Fehler in Zeile " << row << ": " << why << "\n"
        << line << "\n"
        << std::string(column, ' ') << "^";
    msg_ = out.str();    
  }
  const char* what() const { return msg_.c_str(); }
private:
  std::string msg_;
};
 
#endif // PARSEERR_HPP
//~

Ein einfaches Testprogramm zeigt z.B., dass beim ß ein Fehler gefunden wurde:

// #define PARSEERRTEST
#ifdef PARSEERRTEST
 
//: errtst.cpp : dokgen Fehlermeldungen - R.Richter 2002-11-18
/////////////////////////////////////////////////////////////////////
 
#include <iostream>
#ifndef ALL_IN_ONE
  #include "parseerr.hpp"
#endif // ALL_IN_ONE
 
int main()
{
  ParseError err("Was weiß ich", 1, 7, "Was weiß ich");
  std::cout << err.what() << '\n';
  return 0;
}
//~
#endif // PARSEERRTEST

Lexikalische Analyse

Der Eingabestrom wird in kleine Einheiten (Symbole, engl. token) zerlegt, die vom Parser interpetiert werden. Die Zeilen sollten weniger als 300 Zeichen enthalten.

//: lexer.hpp : dokgen lexikalische Analyse - R.Richter 2002-11-18
/////////////////////////////////////////////////////////////////////
 
#ifndef LEXER_HPP
#define LEXER_HPP
 
#include <iostream>
#include <string>
#ifndef ALL_IN_ONE
  #include "parseerr.hpp"
#endif // ALL_IN_ONE
 
class Lexer
{
public:
  Lexer(std::istream& input);
 
  const char* next_token(); // try to read token and return it as ...
  const char* token() const { return token_; }
 
  enum Tokentype { END, NEWLINE, CHAR, TAG };
  enum State { EOT, DOC, CODE };
 
  Tokentype token_type() const; 
  State state() const;
 
  ParseError error(std::string why) const;
private:
  void next_line();
 
  enum { MAXLENGTH = 300 };
  char line_ [MAXLENGTH+1];
  char token_[MAXLENGTH+1];
  int row_, column_;
  std::istream& input_;
};
 
// ---[ implementation details ]-------------------------------------
 
inline
Lexer::Lexer(std::istream& input)
: input_(input)
, row_(0)
{
  token_[0] = '\n';
  token_[1] = '\0';
  next_token();
}
 
inline
ParseError Lexer::error(std::string why) const
{
  return ParseError(why, row_, column_, line_);
}
 
inline
void Lexer::next_line()
{
  line_[0] = '\0';                 
  input_.getline(line_, MAXLENGTH);
  ++row_;
  column_ = 0;
}
 
#endif // LEXER_HPP
//~

Die eingelesenen Zeilen werden in Quelltext und Dokumentation unterteilt. Codezeilen werden als Ganzes übernommen. Dokumentationstext wird weiter untergliedert bis zu Einzelzeichen und Textmarken (Tags).

//: lexer.cpp : dokgen lexikalische Analyse - R.Richter 2002-11-18
/////////////////////////////////////////////////////////////////////
 
#include <cctype>
#include <cstring>
#ifndef ALL_IN_ONE
  #include "lexer.hpp"
#endif // ALL_IN_ONE
 
// enum Tokentype { END, NEWLINE, CHAR, TAG };
 
Lexer::Tokentype Lexer::token_type() const 
{
  if (EOT  == state()) return END;
  if (CODE == state()) return CHAR;
 
  // in DOC line ...
  if (0 == std::strcmp(token(), "\n\n")) return NEWLINE;
  if ('@' == token()[0] && std::isalpha(token()[1])) return TAG;
  /* else */ return CHAR;
}
 
// enum State { EOT, DOC, CODE };
 
Lexer::State Lexer::state() const
{
  // depends on line and input stream
  if (!input_) return EOT;
  if (0 == std::strncmp(line_, "///", 3) && '/' != line_[3]) return DOC;
  return CODE;
}
 
const char* Lexer::next_token()        
{
  if ('\n' == token_[0])               // wenn vorher Zeilenumbruch,
  {
    next_line();                       // dann neue Zeile holen
    if (DOC == state())                // in Dok-Zeile 
    {
      column_ = (' ' != line_[3])? 3:4; // "/// " überlesen
    }
  }
 
  token_[0] = line_[column_++];        // 1 Zeichen holen,
  int n = 1;
 
  if ('\0' == token_[0])               // bei Zeilenende 
  {
    token_[0] = '\n';                  // '\n' in token kopieren  
    if (DOC == state() && '\n' == input_.peek())
    {
      token_[n++] = input_.get();      // evtl. Leerzeile als Absatz
    }
  }
  else if (CODE == state())
  {
    while ('\0' != line_[column_])     // ganze Codezeile auf einmal
    {
      token_[n++] = line_[column_++];
    }
  }
  else if (DOC == state())              
  {
    if('@' == token_[0])               // Tag
    {
      while (std::isalpha(line_[column_])) // Tagname
      {
        token_[n++] = line_[column_++];
      }
    }
  }
  token_[n] = '\0';                    // Zeichenkette abschließen  
  return token();
}                                      
//~

Das Testprogramm zeigt, welche Token erkannt werden:

// #define LEXERTEST
#ifdef LEXERTEST
 
//: lexertst.cpp : Dokgen Testprogramm Lexer - R.Richter 2002-11-19
/////////////////////////////////////////////////////////////////////
 
#include <iostream>
#include <string>
#include <sstream>
 
#ifndef ALL_IN_ONE
  #include "lexer.hpp"
#endif // ALL_IN_ONE
 
int main() 
{
  std::string teststring = 
  "/// @title Test: ÄÖÜäöüß @\n"
  "\n"
  "/// === Das Testdokument\n"
  "/// dient als Testbeispiel\n"
  "/// * Zwei Möglichkeiten:\n"
  "/// *# Es klappt sofort.\n"
  "/// *# Es geht schief.\n"
  "/// > Es geht immer schief.\n"
  "/// >--- Murphy\n"
  "/// :: Probiere nochmal\n"
  "/// : solange es schiefgeht\n"
  "/// Quelltextzeilen beginnen nicht mit @code{///}\n"
  "/// und werden in @code{Maschinenschrift} gesetzt:\n"
  "// C++ Kommentare @title\n"
  "////////////////////////\n"
  "int main() { return 0; }\n"
  "/* endet mit star slash */";
 
  std::istringstream input( teststring );
  Lexer lex(input);
 
  std::cout << "state \ttype \tchar \ttoken\n";
 
  while (Lexer::END != lex.token_type())
  {
    std::cout << lex.state()      << '\t' 
              << lex.token_type() << '\t';
 
    unsigned char first = *lex.token();
    if (std::isprint(first) && !std::isspace(first))
    {
      std::cout << first;
    }
    else
    {
      std::cout << "ASC " << int(first);
    }
    std::cout << '\t' << lex.token() << '\n';
 
    if ('\n' == lex.token()[0])
    {
      // ist nun wirklich kein Fehler ... nur Debugging
      std::cout << lex.error("Zeile verarbeitet:").what() << '\n'; 
    }
 
    lex.next_token();
  }
  return 0;
}
//~
#endif // LEXERTEST

Einfache Ausgabe

Dokumente in einen Ausgabestrom zu schicken, ist Aufgabe dieser Klasse. Für die Elemente eines Dokumentes werden Ausgabemethoden festgelegt. Die Klasse dient als Ausgangspunkt für bestimmte Ausgabeziele, z.B. HTML-Ausgabe.

//: target.hpp : dokgen Basis Ausgabeziel - R.Richter 2002-11-20
/////////////////////////////////////////////////////////////////////
 
#ifndef TARGET_HPP
#define TARGET_HPP
 
#include <iostream>
#include <string>
 
class Target                               // mit Debug-Ausgaben:
{
public:
  Target(std::ostream& output)
  : output_(output)
  {
  }
  ~Target()
  {
    output_.flush();
  }
  virtual void document_start()            
  {
    output_ << "DOCUMENT Start\n"; 
  }
  virtual void make_title(std::string author, 
                          std::string title,
                          std::string keywords) 
  {
    output_ << "AUTHOR: " << author << '\n'
            << "TITLE : " << title  << '\n'
            << "KEYWORDS: " << keywords << '\n';
  }
  virtual void translate(std::string text) 
  {
    output_ << text; 
  }
 
  virtual void paragraph_start()            
  { 
    output_ << "\nPARAGRAPH\n"; 
  }
  virtual void paragraph_end()            
  { 
    output_ << "\nPARAGRAPH END\n"; 
  }
  virtual void next_line()                 
  {
    output_ << "\n"; 
  }
  virtual void label(std::string text)     
  {
    output_ << "LABEL:(" << text << ")"; 
  }
  virtual void link(std::string url, std::string caption)       
  {
    output_ << "LINK:(" << url << " | " << caption << ")"; 
  }
  virtual void image(std::string name, std::string caption)     
  {
    output_ << "IMAGE:(" << name  << " | " << caption << ")"; 
  }
  virtual void ref(std::string label, std::string caption)      
  {
    output_ << "REF:(" << label  << " | " << caption << ")"; 
  }
 
  virtual void section_start(std::string title)  
  {
    output_ << "SECTION:(" << title << ")"; 
  }
  virtual void subsection_start(std::string title) 
  {
    output_ << "SUBSECTION:(" << title << ")"; 
  }
 
  virtual void subsubsection_start(std::string title)  
  {
    output_ << "SUBSUBSECTION:(" << title << ")\n"; 
  }
  virtual void subsubsubsection_start(std::string title) 
  {
    output_ << "SUBSUBSUBSECTION:(" << title << ")\n"; 
  }
 
  virtual void section_end(std::string title)  
  {
    output_ << "SECTION_END:(" << title << ")\n"; 
  }
  virtual void subsection_end(std::string title) 
  {
    output_ << "SUBSECTION_END:(" << title << ")\n"; 
  }
 
  virtual void subsubsection_end(std::string title)  
  {
    output_ << "SUBSUBSECTION_END:(" << title << ")\n"; 
  }
  virtual void subsubsubsection_end(std::string title) 
  {
    output_ << "SUBSUBSUBSECTION_END:(" << title << ")\n"; 
  }
 
  virtual void inlinecode(char begin, std::string text, char end)  
  {
    output_ << "CODE: " << begin << text << end; 
  }
 
  virtual void code_start()                
  {
    output_ << "CODE Start \n"; 
  }
  virtual void code(std::string text)      
  {
    output_ << text; 
  } 
  virtual void code_end()                  
  {
    output_ << "CODE End \n"; 
  }
 
  virtual void table_start()               
  {
    output_ << "TABLE Start\n"; 
  }
  virtual void table_cell_start(char alignment=' ')            
  {
    output_ << "TABLE Cell Start"<< alignment<<"\n"; 
  }
  virtual void table_cell_end()            
  {
    output_ << "TABLE Cell End\n"; 
  }
  virtual void table_row_start()             
  {
    output_ << "TABLE Row Start\n"; 
  }
  virtual void table_row_end()             
  {
    output_ << "TABLE Row End\n"; 
  }
  virtual void table(std::string text)     
  {
    output_ << text; 
  } 
  virtual void table_end()                 
  {
    output_ << "TABLE End\n"; 
  }
 
  virtual void list_start(char type)
  {
    switch (type)
    {
    case '*': break;
    case '#': output_ << "NUM"; break;
    case ';': output_ << "DEFINITION"; break;
    default : break;
    }
    output_ << "LIST Start\n"; 
  }
 
  virtual void list_item_start(char type, std::string text)           
  {
    switch (type)
    {
    case '*': // FALLTHROUGH
    case '#': output_ << "LIST Item Start\n"; break;
    case ';': 
      output_ << "DEFINITIONLIST Item " << text << " Start\n"; 
      break;
    default : break;
    }
  }
 
  virtual void list(std::string text)      
  {
    output_ << text; 
  } 
 
  virtual void list_item_end(char type)             
  {
    switch (type)
    {
    case '*': // FALLTHROUGH
    case '#': 
    case ';': output_ << "LIST Item End\n"; break;
    default : break;
    }
  }
 
  virtual void list_end(char type)                  
  {
    switch (type)
    {
    case '*': break;
    case '#': output_ << "NUM"; break;
    case ';': output_ << "DEFINITION"; break;
    default : break;
    }
    output_ << "LIST End\n"; 
  }
 
  virtual void quote_start()               
  {
    output_ << "QUOTE Start\n"; 
  }
  virtual void quote(std::string text)     
  {
    output_ << text; 
  } 
  virtual void quote_from_start()               
  {
    output_ << "QUOTE from Start\n"; 
  }
  virtual void quote_from_end()               
  {
    output_ << "QUOTE from Start\n"; 
  }
  virtual void quote_end() 
  {
    output_ << "\nQUOTE End\n"; 
  }
 
  virtual void indentation_start()
  {
    output_ << "\nIndented text\n";         
  }
 
  virtual void indent(int depth = 1, bool firstline = false)
  {
    output_ << std::string(2*depth,' ');
  }
 
  virtual void indentation_end()
  {
    output_ << "\nIndented text end\n";         
  }
 
  virtual void document_end()              
  {
    output_ << "DOCUMENT End\n"; 
  }
 
protected:
  std::ostream& output_;
};
 
#endif // TARGET_HPP
//~

Syntax-Analyse

Dies ist der zentrale Teil eines Übersetzers. Das Durchmustern (engl. parse) der Eingabe setzt die lexikalische Analyse voraus.

//: parser.hpp : dokgen syntaktische Analyse - R.Richter 2002-11-20
/////////////////////////////////////////////////////////////////////
 
#ifndef PARSER_HPP
#define PARSER_HPP
#include <string>
 
#ifndef ALL_IN_ONE
  #include "lexer.hpp"
  #include "target.hpp"
#endif // ALL_IN_ONE

Die vom Lexer erkannten Symbole der Sprache müssen einer formalen Grammatik (Syntax) genügen:

/*
// --- syntactic elements
 
document ::= dochead { textelement } { section } END
 
dochead ::= { docheadtag { space } docheadline } [ newparagraph | not_DOC ]
 
docheadtag ::=   "@author" 
               | "@title" 
               | "@keywords"
 
docheadline ::= '{' { docheadline } '}' { space } [ newline ]
                | space | docchar } [ newline ]
 
section ::= sectionhead { textelement } { section }
 
sectionhead ::= sectionlevel { space } docheadline [ newline ]
 
sectionlevel ::= "===" | "====" | "=====" | "======"
 
textelement ::= sourcecode 
                | table | list 
                | quote | indentedtext
                | paragraph
 
sourcecode ::= { anychar | '\n' } not_CODE
 
paragraph ::= { docline } [ newline ]
 
docline ::=  { texttoken } [ newline ]
 
texttoken ::= space | docchar | taggedelement
 
taggedelement ::= codesnippet | image | link | reference | label 
 
codesnippet ::= "@code" { space } codelimiter { docchar } codelimiter
 
codelimiter ::= '{' | '}' | docchar
                // as pair  // same char before and after
 
image ::= "@image" { space } URL caption
 
link ::= "@link" { space } URL caption
 
reference ::= "@ref" { space } URL caption
 
label ::= "@label" { space } URL 
 
URL ::= { nonspace } 
 
caption ::= { space } [ '{' docchar '}' ]
 
 
table ::= { tablerow }
 
tablerow ::= { '|' [ cellalignment ] { texttoken } } newline
 
cellalignment ::= '<' | '^' | '>'
 
 
list ::= { { prevlistleveltag } listtype listitem }
 
prevlistleveltag ::= '*' | '#' | ';'   // all but last tag
 
listtype ::=   '*'
             | '#'
             | ';' defineditem
 
definededitem ::= { docchar | space } ':'
 
listitem ::=   docline
             | '{' listelement '}' { space } newline
 
listelement ::=   sourcecode | listline
                | table | list 
                | quote | indentedtext
 
listline ::=  { listtoken } [ newline ]
 
listtoken ::= space | docchar_but_not_closing_brace | taggedelement  
 
quote ::= { '>' docline } [ ">---" quotedfrom ]
 
quotedfrom ::= docline
 
indentedtext ::= { ':' { ':' } docline }                 
 
 
// --- lexical elements
 
space        ::= '\t' | ' ' in DOC state
newline      ::= '\n' in DOC state
newparagraph ::= "\n\n" in DOC state
docchar      ::= anychar and state == DOC 
code         ::= lines of any_char in CODE state, separated by '\n'
*/
 
class Parser
{
public:
  Parser(Lexer& lexer, Target& target)
  : lexer_(lexer), target_(target)
  {
  }
  void document();
private:
  void dochead();
  std::string docheadline(std::string& text);
 
  int section(int level = 0);
  int sectionlevel();
  void textelement();
 
  void paragraph();
  void sourcecode();
  void docline();
 
  void taggedelement();
  void image();
  void reference();
  void link();
  void label();
  void codesnippet();
  std::string URL(std::string& text);
  std::string caption(std::string& text);
 
  void table();
  void list();
  void adapt_list_level(char old[], const char actual[]); 
  void listline();
 
  void quote();
  void indentedtext();
 
  void spaces();
  void brace(char paren); // expects parenthesis 
  std::string collect(std::string& text, char end);
  std::string nonspace(std::string& text);
 
  void debug(std::string msg);
 
  Lexer&  lexer_;
  Target& target_;
};
 
#endif // PARSER_HPP
//~

Die Methoden der Klasse entsprechen den syntaktischen Elementen des Eingabe-Dokuments. Das Durchmustern wird mit der syntaktischen Wurzel document() angestoßen.

//: parser.cpp : dokgen syntaktische Analyse - R.Richter 2002-11-20
/////////////////////////////////////////////////////////////////////
 
#include <cctype>
#include <cstring>
#ifndef ALL_IN_ONE
  #include "parser.hpp"
#endif // ALL_IN_ONE
 
void Parser::debug(std::string msg)
{
  std::cerr << msg << '\n'
            << "state \ttype \tchar \ttoken" << '\n'
            << lexer_.state()     << '\t' 
            << lexer_.token_type() << '\t';
 
  unsigned char first = *lexer_.token();
  if (std::isprint(first) && !std::isspace(first))
  {
    std::cerr << first;
  }
  else
  {
    std::cerr << "ASC " << int(first);
  }
  std::cerr << '\t' << lexer_.token() << '\n';
  if ('\n' == lexer_.token()[0])
  {
    // ist nun wirklich kein Fehler ... nur Debugging
    std::cerr << lexer_.error("Zeile verarbeitet:").what() << '\n'; 
  }
}

Ein Dokument beginnt mit einem Dokumentenkopf. Nach beliebig vielen Textelementen wird das Dokument durch Abschnittsüberschriften gegliedert.

void Parser::document()
{
  // document ::= dochead { textelement }{ section } END
 
  target_.document_start();
  dochead();
 
  // before EOT or '=' in DOC state ...
 
  while (Lexer::END != lexer_.token_type() &&
         !(Lexer::DOC == lexer_.state() &&
           '=' == *lexer_.token()
          )
        )
  {
    textelement();
  }
  // now reached EOT or '=' in DOC state 
 
  if (Lexer::DOC == lexer_.state() &&
      '=' == *lexer_.token()
     )
  {
    int level = 0;
    while (Lexer::DOC == lexer_.state())
    {
      level = section(level); 
    }
  }
  target_.document_end();
}

Der Dokumentkopf wird erstellt, wenn alle Kopfangaben festgelegt sind bzw. sobald Inhalte erkannt werden, die nicht zum Dokumentkopf gehören (Quelltext, normale Erläuterungen).

void Parser::dochead()
{
  // dochead ::= { docheadtag { space } docheadline } [ newparagraph | not_DOC ]
 
  // docheadtag ::=   "@author" 
  //                | "@title" 
  //                | "@keywords"
 
  // dochead ends when all docheadtags are defined,
  // or doctext element
  // or when in state CODE or EOT
 
  std::string author, keywords, title;
 
  // solange in state DOC und Autor, Titel oder Schlüssel unbekannt
  while (   Lexer::DOC == lexer_.state()
         && ("" == author || "" == title || "" == keywords) 
        )
  {
    if (Lexer::TAG == lexer_.token_type())
    {
      // falls authortag, Autor einlesen
      // falls titletag, Titel einlesen  (Titellänge nur eine Zeile?)
      std::string *text = 0;
 
      if (std::string(lexer_.token()) == "@author"  ) text = &author; 
      if (std::string(lexer_.token()) == "@keywords") text = &keywords;
      if (std::string(lexer_.token()) == "@title"   ) text = &title;
 
      if (text)  // Tag erkannt
      {
        lexer_.next_token(); 
        spaces();
        docheadline(*text);
      }
      else break; // andere Tags im Kopf nicht erlaubt, anfangen
    }
    else break;   // falls irgendwas anderes, anfangen
  }
  target_.make_title(author, title, keywords);
 
  if (Lexer::NEWLINE == lexer_.token_type())
  {
    lexer_.next_token();
  }
}
 
std::string Parser::docheadline(std::string& text)
{
  // docheadline ::= '{' { docheadline } '}' { space } [ newline ]
  //                 | { space | docchar } [ newline ]
 
  char begin = *lexer_.token();
  char end = '\n';
 
  if ('{' == begin)
  {
    brace(begin);
    spaces();
    end = '}';
  }
 
  while (end != *lexer_.token()) 
  {
    if (Lexer::DOC != lexer_.state())
    {
      throw lexer_.error("char must be in DOC line");
    }
    text += lexer_.token();
    lexer_.next_token();
  }
  brace(end);
  if ('}' == end)
  {
    spaces();
    if (Lexer::NEWLINE != lexer_.token_type() && '\n' == *lexer_.token())
    {
      target_.translate("\n");
      lexer_.next_token();
    }
  }
  return text;
}

Abschnittsüberschriften beginnen mit mehreren Gleichheitszeichen. Abschnitte können verschachtelt werden.

int Parser::section(int level)
{
  // section ::= sectionhead { textelement } { section }
  // sectionhead ::= sectionlevel { space } docheadline [ newline ]
 
  // sectionhead
 
  if (level == 0) // otherwise already read
  {
    level = sectionlevel();       
  }
  spaces();
 
  std::string text;
  docheadline(text);
 
  switch (level)
  {
  case 3: target_.section_start(text); break;
  case 4: target_.subsection_start(text); break;
  case 5: target_.subsubsection_start(text); break;
  case 6: target_.subsubsubsection_start(text); break;
  default:
    lexer_.error("invalid section level");
    break;
  } 
 
  if (Lexer::NEWLINE == lexer_.token_type())
  {
    lexer_.next_token();
  }
 
  // textelements
 
  while (Lexer::END != lexer_.token_type() &&
         !(Lexer::DOC == lexer_.state() &&
           '=' == *lexer_.token()
          )
        )
  {
    textelement();
  }
 
  // subsections
 
  int next_level = 0;
  if (Lexer::DOC == lexer_.state() &&
      '=' == *lexer_.token()
     )
  {
     next_level = sectionlevel(); 
     while (next_level > level)
     {
       next_level = section(next_level);   
     }
  }
  // section end
  switch (level)
  {
  case 3: target_.section_end(text); break;
  case 4: target_.subsection_end(text); break;
  case 5: target_.subsubsection_end(text); break;
  case 6: target_.subsubsubsection_end(text); break;
  default:
    lexer_.error("invalid section level");  // should not happen
    break;
  }
  return next_level;
}

Die Überschriftenart wird durch die Anzahl der Gleichheitszeichen festgelegt.

int Parser::sectionlevel()
{
  // sectionlevel ::= "===" | "====" | "=====" | "======"
 
  int level = 0;
  while ('=' == *lexer_.token())
  {
    ++level;
    lexer_.next_token();
  }
  return level;
}

Fließtext besteht aus Absätzen, Tabellen, Listen, Zitaten, eingerücktem Text oder Quelltextzeilen.

void Parser::textelement()
{
  // textelement ::= sourcecode 
  //                 | table | list 
  //                 | quote | indentedtext
  //                 | paragraph
 
  if (Lexer::CODE == lexer_.state())
  {
    sourcecode();
  }
  else
  {
    switch (*lexer_.token())
    {
    case '|': table(); break;
    case '*':
    case '#':
    case ';': list();  break;
    case '>': quote(); break;
    case ':': indentedtext(); break;
    default : paragraph();    break;
    }
  }
}

In jedem Abschnitt ist am Zeilenanfang zu entscheiden, welche Art von Textelement als nächstes zu verarbeiten ist:

void Parser::paragraph()
{
  //paragraph ::= { docline } [ newline ]
 
  target_.paragraph_start();
 
  while (Lexer::DOC == lexer_.state() && 
         !strchr("|*#;>:=", *lexer_.token()) &&
         Lexer::NEWLINE != lexer_.token_type()     
        )
  {
    docline();  
  }
 
  if (Lexer::NEWLINE == lexer_.token_type())
  {
    lexer_.next_token();
  }
  target_.paragraph_end();
}

Quelltextabschnitte werden zeilenweise weitergegeben:

void Parser::sourcecode()
{
  // sourcecode ::= { anychar | '\n' } not_CODE
 
  target_.code_start();
 
  while (Lexer::CODE == lexer_.state())
  {
    target_.code(lexer_.token());
    lexer_.next_token();
  }
  target_.code_end();
}

"Normale" Zeilen bestehen aus Zeichen oder Textmarken

void Parser::docline()
{
  // docline ::=  { texttoken } [ newline ]
  // texttoken ::= space | docchar | taggedelement
 
  while (Lexer::DOC == lexer_.state() && '\n' != *lexer_.token())
  {
    if ('@' == *lexer_.token())
    {
      taggedelement();
    }
    else
    {
      if ('\\' == *lexer_.token()) // linebreak
      {
        lexer_.next_token();
        if ('\n' == *lexer_.token())
        {
          target_.next_line(); 
        }
        else
        {
          target_.translate("\\"); 
        }
      }
      else
      {
        target_.translate(lexer_.token());
        lexer_.next_token();
      }
    }
  }
  if (Lexer::NEWLINE != lexer_.token_type())
  {
    target_.translate("\n");
 
    std::cerr << lexer_.token() << "\n";
    lexer_.next_token();
  }
}

Textmarken enthalten Quelltextschnipsel, Bilder oder Verweise:

void Parser::taggedelement()
{
  // taggedelement ::= codesnippet | image | link | reference | label 
 
  if (     std::string(lexer_.token()) == "@code" ) codesnippet();
  else if (std::string(lexer_.token()) == "@image") image(); 
  else if (std::string(lexer_.token()) == "@ref"  ) reference();
  else if (std::string(lexer_.token()) == "@link" ) link();
  else if (std::string(lexer_.token()) == "@label") label();
  else 
  {
    target_.translate(lexer_.token()); // normal char
    lexer_.next_token();
  }
}

Die Marke @code kann statt geschweifter Klammern (Standard) auch ein beliebiges anderes Zeichen als Begrenzer akzeptieren.

void Parser::codesnippet()
{
  // codesnippet ::= "@code" { space } codelimiter { docchar } codelimiter
 
  // codelimiter ::= '{' | '}' | docchar
                // as pair  // same char before and after
 
  lexer_.next_token();
  spaces();
 
  char begin = '{', end = '}';
 
  if ('{' != *lexer_.token())
  {
    begin = end = *lexer_.token();
  }
  lexer_.next_token();
 
  std::string text;
  target_.inlinecode(begin, collect(text, end), end);
}

Bilder und Verweise referenzieren einen Namen. Nachfolgend kann in geschweiften Klammern ein beschreibender Text eingegeben werden.

void Parser::image()
{
  // image ::= "@image" { space } URL caption 
 
  lexer_.next_token();
  spaces();
 
  std::string name, text;
  URL(name);
  caption(text);
  target_.image(name, text);
}
 
void Parser::reference()
{
  // reference ::= "@ref" { space } URL caption
 
  lexer_.next_token();
  spaces();
 
  std::string name, text;
  URL(name);
  caption(text);
  target_.ref(name, text);
}
 
void Parser::link()
{
  // link ::= "@link" { space } URL caption
 
  lexer_.next_token();
  spaces();
 
  std::string name, text;
  URL(name);
  caption(text);
  target_.link(name, text);
}
 
void Parser::label()
{
  // label ::= "@label" { space } URL 
 
  lexer_.next_token();
  spaces();
 
  std::string name;
  URL(name);
  target_.label(name);
}
 
std::string Parser::URL(std::string& text)
{
  // URL ::= { nonspace } 
 
  return nonspace(text);
}
 
std::string Parser::caption(std::string& text)
{
  // caption ::= { space } [ '{' docchar '}' ]
 
  spaces();
  if ('{' == *lexer_.token())
  {
    lexer_.next_token();
    collect(text, '}');
  }
  return text;
}

Innerhalb einer Tabelle bewirken senkrechte Striche den Übergang zur nächsten Zelle, Zeilenvorschübe den Übergang in die nächste Zeile. Mehrspaltige bzw. mehrzeilige Bereiche sind nicht vorgesehen.

void Parser::table()
{
  // table ::= { tablerow }
 
  // tablerow ::= { '|' [ cellalignment ] { texttoken } } newline
 
  // cellalignment ::= '<' | '^' | '>'
 
  target_.table_start();
  while ('|' == *lexer_.token())
  {
    target_.table_row_start();
 
    while ('\n' != *lexer_.token())
    {
      lexer_.next_token();
 
      char align = ' ';
 
      switch (*lexer_.token())
      {
      case '<':
      case '>':
      case '^':
        align = *lexer_.token();
        lexer_.next_token();
        break;
      default: break;
      }
      target_.table_cell_start(align);
 
      while ('|' != *lexer_.token() && '\n' != *lexer_.token())
      {
        if (Lexer::TAG == lexer_.token_type())
        {
          taggedelement();  
        }
        else
        {
          target_.translate(lexer_.token());
          lexer_.next_token();
        }
      }
      target_.table_cell_end();
    }
 
    if (Lexer::NEWLINE != lexer_.token_type())
    {
      lexer_.next_token();
    }
     target_.table_row_end();
  }
  target_.table_end();
}

Listeneinträge beginnen mit Stern, Doppelkreuz oder Semikolon. Mehrzeilige Einträge müssen geklammert werden.

void Parser::list()
{
  // list ::= { { prevlistleveltag } listtype listitem }
 
  // prevlistleveltag ::= '*' | '#' | ';'   // all but last tag
 
  // listtype ::=   '*'
  //              | '#'
  //              | ';' defineditem
 
  // definededitem ::= { docchar | space } ':'
 
  const int MAXDEPTH = 100;
  char previoustags[1+MAXDEPTH] = "";
  char actualtags  [1+MAXDEPTH] = "";
 
  while (Lexer::DOC == lexer_.state() && std::strchr("*#;", *lexer_.token()))
  {
    // read actual head 
    int level=0;
    while (std::strchr("*#;", *lexer_.token()))
    {
      actualtags[level++] = *lexer_.token();
      if (level >= MAXDEPTH) throw lexer_.error("maximum list depth exceeded");
      lexer_.next_token();
    }
    actualtags[level] = '\0';
    adapt_list_level(previoustags, actualtags);
 
    char type = actualtags[level-1]; 
 
    if (';' == type) // defined item
    {
      std::string definedname;
      collect(definedname, ':');          
      target_.list_item_start(type, definedname);
    }
    else
    {
      target_.list_item_start(type, "");
    }
    spaces();
 
    // listitem ::=   docline
    //              | '{' listline { listelement } '}' { space } newline
 
    // listelement ::=   sourcecode | listline
    //                 | table | list 
    //                 | quote | indentedtext
 
    if ('{' == *lexer_.token()) // process item
    {
      lexer_.next_token(); 
      listline();
 
      while ('}' != *lexer_.token())
      {
        if (Lexer::CODE == lexer_.state())
        {
          sourcecode();
        }
        else
        {
          switch (*lexer_.token())
          {
          case '|': table(); break;
          case '*':
          case '#':
          case ';': list();  break;
          case '>': quote(); break;
          case ':': indentedtext(); break;
          default : 
            listline();  
            break;
          }
        }
      }
      brace('}');
      spaces();
      if (level > 1 || Lexer::NEWLINE != lexer_.token_type())
      {
        lexer_.next_token();  // eat newline
      }       
    }
    else
    {
      docline();
    }
    // 2007-03-16 (-): target_.list_item_end(type);
  }
  actualtags[0] = '\0';                       
  adapt_list_level(previoustags, actualtags); // Listen schliessen
}
 
void Parser::adapt_list_level(char old[], const char actual[])
{
  // count matching chars
  int matching = 0;
  while (old[matching] && old[matching] == actual[matching])
  {
    ++matching; 
  }
 
  int oldlevel = strlen(old);
  int actuallevel = strlen(actual);
 
  // close nonmatching list type levels
 
  while (matching < oldlevel)
  {
    char oldtype = old[--oldlevel];    
    target_.list_item_end(oldtype);  
    target_.list_end(oldtype);
    old[oldlevel] = '\0'; 
  }
 
  // close list item of same type on same level
 
  if (matching > 0 &&
      matching == actuallevel)  
  {
    target_.list_item_end(old[matching-1]);
  }
  // else ...
 
  // open list level(s)
  if (actuallevel > matching + 1)  
  {
    // warning: you should open list levels one by one
    throw lexer_.error("list level should be increased by 1 only");
  }
 
  while (matching < actuallevel)
  {
    target_.list_start(actual[matching]);
    old[matching] = actual[matching];
    old[++matching] = '\0';
  }
}
 
void Parser::listline()
{
  // listline ::=  { listtoken } [ newline ]
  // listtoken ::= space | docchar_but_not_closing_brace | taggedelement  
 
  while (Lexer::DOC == lexer_.state() && '}' != *lexer_.token())
  {
    if ('@' == *lexer_.token())
    {
      taggedelement();
    }
    else
    {
      if ('\\' == *lexer_.token()) // linebreak
      {
        lexer_.next_token();
        if( '\n' == *lexer_.token() )
        {
          target_.next_line(); 
        }
        else
        {
          target_.translate("\\"); 
        }
      }
      else
      {
        target_.translate(lexer_.token());
        lexer_.next_token();
      }
    }
  }
  if (Lexer::NEWLINE == lexer_.token_type())
  {
    target_.next_line();
    lexer_.next_token();
  }
}

Zitate sind durch > markiert.

void Parser::quote()
{
  // quote ::= { '>' docline } [ ">---" quotedfrom ]
 
  // quotedfrom ::= docline
 
  target_.quote_start();
 
  while (Lexer::DOC == lexer_.state() && '>' == *lexer_.token())
  {
    lexer_.next_token();
 
    if ('-' == *lexer_.token())
    {
      while ('-' == *lexer_.token())
      {
        lexer_.next_token();
      }
      target_.quote_from_start();
      docline();
      target_.quote_from_end();
    }
    else
    {
      docline();
    }
  }
  target_.quote_end();
}

Eine Folge von Doppelpunkten bewirkt Einrückung der Textzeile

void Parser::indentedtext()
{
  // indentedtext ::= { ':' { ':' } docline }                 
  target_.indentation_start();
  int lineNr = 0;
  while (Lexer::DOC == lexer_.state() && ':' == *lexer_.token())
  {
    int depth = 0;
    while (':' == *lexer_.token())
    {
      ++depth;
      lexer_.next_token();
    }
    target_.indent(depth, 0 == lineNr++);
    docline();
  }
  target_.indentation_end();
}

An einigen Stellen müssen Leerzeichen in der Dokumentation übergangen werden:

void Parser::spaces()
{
  while (Lexer::EOT != lexer_.state() && std::strchr(" \t", *lexer_.token()))
  {
    lexer_.next_token(); // Leerzeichen auffressen
  }
}

Marken enden bzw. beginnen mit einem bestimmten Zeichen. Wenn dieses nicht gefunden wird, ist ein Fehler zu melden.

void Parser::brace(char paren)
{
  if (*lexer_.token() == paren && Lexer::DOC == lexer_.state())
  {
    lexer_.next_token();
  }
  else
  {
    std::string errormsg = "X expected";
    errormsg[0] = paren;
    throw lexer_.error(errormsg);
  }
}

Der Text bis zum End-Zeichen wird aufgesammelt und in der Zeichenkette abgelegt. Der Text darf die vorgegebene Länge nicht überschreiten.

std::string Parser::collect(std::string& text, char end)
{
  while (*lexer_.token() != end) 
  {
    if (lexer_.state() != Lexer::DOC)
    {
      throw lexer_.error("char must be in DOC line");
    }
    text += lexer_.token();
    lexer_.next_token();
  }
  brace(end);
  return text;
}

Text bis zu einem Leerzeichen wird aufgesammelt:

std::string Parser::nonspace(std::string& text)
{
  while (!isspace(*lexer_.token())) 
  {
    if (lexer_.state() != Lexer::DOC)
    {
      throw lexer_.error("char must be in DOC line");
    }
    text += lexer_.token();
    lexer_.next_token();
  }
  return text;
}
//~

Der Parser arbeitet mit Lexer als Eingabegerät und Target als Ausgabegerät zusammen:

// #define PARSETEST
#ifdef  PARSETEST
 
//: parsetst.cpp : Testprogramm Lexer - R.Richter 2002-11-20
//////////////////////////////////////////////////////////////////////////
 
#include <cstdlib>
#include <iostream>
#include <sstream> 
#ifndef ALL_IN_ONE
  #include "parser.hpp"
#endif // ALL_IN_ONE
 
int main() 
{
  std::string teststring =
  "/// @title Test: ÄÖÜäöüß @\n"
  "\n"
  "/// @image onesource.bmp { blabla }\n"
  "\n"
  "/// === Das Testdokument\n"
  "/// dient als Testbeispiel\n"
  "/// * Zwei Möglichkeiten:\n"
  "/// *# Es klappt sofort.\n"
  "/// *# { Es geht schief.\n"
  "///      Das ist normal...\n"
  "///    }  \n"
  "/// > Es geht immer schief.\n"
  "/// >--- Murphy\n"
  "/// :: Probiere nochmal\n"
  "/// : solange es schiefgeht\n"
  "/// | Tabelle | 3 Spalten | ausgerichtet\n"
  "/// |< links  |^Mitte |> rechts\n"
  "\n"
  "/// ==== 1.1 Unterabschnitt\n"
  "/// ==== 1.2 Unterabschnitt\n"
  "/// @link dokgen.htm { Dokumentation }\n"
  "\n" 
  "/// === Abschnitt Quellcode\n"
  "/// Quelltextzeilen beginnen nicht mit @code{///}\n"
  "/// und werden in @code{Maschinenschrift} gesetzt:\n"
  "// C++ Kommentare @title\n"
  "////////////////////////\n"
  "int main() { return 0; }\n"
  "/* endet mit star slash */";
 
  std::istringstream input(teststring);
  Lexer lex(input);
  Target target(std::cout);
  Parser parser(lex, target);
  try
  {
    parser.document();
  }
  catch(ParseError e)
  {
    std::cerr << e.what() << '\n';
    return EXIT_FAILURE;
  }
  return EXIT_SUCCESS;
}
//~
#endif // PARSETEST

HTML-Ausgabe

Online-Dokumentation im HTML-Format ist eine spezielle, wichtige Form eines Targets. Die Ausformung ist nicht kompliziert, nur mühsam.

//: htmltarget.hpp : dokgen Ausgabeziel HTML - R.Richter 2002-11-20
/////////////////////////////////////////////////////////////////////
 
#ifndef HTMLTARGET_HPP
#define HTMLTARGET_HPP
 
#ifndef ALL_IN_ONE
  #include "target.hpp"
#endif // ALL_IN_ONE
 
class HTMLTarget : public Target
{
public:
  HTMLTarget(std::ostream& output)
  : Target(output)
  {
  }
 
  void document_start()            
  { 
    output_ << 
      "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" " 
      "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
      "<html>\n"
      "<head>\n"
      "  <link rel=stylesheet type=\"text/css\" href=\"docstyle.css\">\n"
      "  <meta http-equiv=\"Content-Type\" content=\"text/html; "
      "charset=iso-8859-1\">\n"
      "  <meta name=\"GENERATOR\" content=\"dokGen 0.10 - R.Richter\">\n";
  }
 
  void make_title(std::string author, std::string title, 
                  std::string keywords) 
  {
    if (author != "")
    {
      output_ << "  <meta name=\"author\" content=\"";
      translate(author);
      output_ << "\">\n";
    }
    if (keywords != "")
    {
      output_ << "  <meta name=\"keywords\" content=\"";
      translate(keywords);
      output_ << "\">\n";
    }
    if (title != "")
    {
      output_ << "<title>";
      translate(title);  
      output_ << "</title>\n";
    }
 
    output_ << 
      "</head>\n"
      "<body><a name=\"top\"></a>\n"
      "\n"
      "<!-- Document starts here -->\n"
      "\n";
 
    if (title != "")
    {
      output_ << "<h1 class=\"title\">";
      translate(title);  
      output_ << "</h1>\n";
    }
    if (author != "")
    {
      output_ << "<h2 class=\"author\">";
      translate(author);
      output_ << "</h2>\n";
    }
    if (keywords != "")
    {
      output_ << "<p>";
      translate("Suchbegriffe: ");
      translate(keywords);
      output_ << "</p>\n";
    }
  }
 
  void next_line()                 
  { 
    output_ << "<br>"; 
  }
 
  void label(std::string text)       
  {
    output_ << "<a name=\"" << text << "\"></a>"; 
  }
 
  void link(std::string url, std::string caption)       
  {
    output_ << "<a href=\"" << url << "\">";
    translate(caption != "" ? caption : url);
    output_ << "</a>"; 
  }
 
  void image(std::string name, std::string caption)     
  {
    output_ << "<img src=\"" << name << "\"";
 
    if (caption != "")
    {
      output_ << " alt = \"" << caption << "\"><br>";
      translate(caption);
    }
    else
    {
      output_ << " alt = \"Bild " << name << "\">";
    }
  }
 
  void ref(std::string label, std::string caption)       
  {
    output_ << "<a href=\"#" << label << "\">";
    translate(caption != "" ? caption : label);
    output_ << "</a>"; 
  }
 
  void section_start(std::string title)     
  {
    output_ << "\n<h3>"; 
    translate(title); 
    output_ << "</h3>"; 
  }
 
  void subsection_start(std::string title)     
  {
    output_ << "\n<h4>";
    translate(title); 
    output_ << "</h4>"; 
  }
 
  void subsubsection_start(std::string title)     
  {
    output_ << "\n<h5>";
    translate(title); 
    output_ << "</h5>"; 
  }
 
  void subsubsubsection_start(std::string title)     
  {
    output_ << "\n<h6>";
    translate(title); 
    output_ << "</h6>"; 
  }
 
  void section_end(std::string title)     
  {
    // do nothing
  }
 
  void subsection_end(std::string title)     
  {
    // do nothing
  }
 
  void subsubsection_end(std::string title)     
  {
    // do nothing
  }
 
  void subsubsubsection_end(std::string title)     
  { 
    // do nothing
  }
 
  void paragraph_start()
  {
    output_ << "\n<p>";
  }
  void paragraph_end()
  {
    output_ << "\n</p>";
  }
 
  void inlinecode(char begin, std::string text, char end)
  {
    output_ << "<code>";
    translate(text);
    output_ << "</""code>"; 
  }
 
  void code_start()                
  {
    output_ << "<pre>"; 
  }
 
  void code(std::string text)      
  { 
    translate(text); 
  } 
 
  void code_end()                  
  {
    output_ << "</pre>\n"; 
  }
 
  void table_start()               
  {
    output_ << "<table border=\"1\">\n"; 
  }
 
  void table_cell_start(char alignment = ' ')               
  {
    output_ << "\t\t<td"; 
 
    switch (alignment)
    {
    case '<': output_ << " align=\"left\""; break;
    case '>': output_ << " align=\"right\""; break;
    case '^': output_ << " align=\"center\""; break;
    default: break; 
    }
    output_ << ">"; 
  }
 
  void table_cell_end()               
  {
    output_ << "\t\t</td>\n"; 
  }
 
  void table_row_start()               
  {
    output_ << "\t<tr>\n"; 
  }
 
  void table_row_end()               
  {
    output_ << "\t</tr>\n"; 
  }
 
  void table(std::string text)     
  {
    translate(text); 
  } 
 
  void table_end()
  {
    output_ << "</table>\n"; 
  }
 
  void list_start(char type)   
  {
    switch (type)
    {
    case '*': output_ << "\n<ul>";break;  
    case '#': output_ << "\n<ol>"; break;
    case ';': output_ << "\n<dl>"; break;
    default: break;
    }
  }
 
  virtual void list_item_start(char type, std::string text)           
  {
    switch (type)
    {
    case '*': // FALLTHROUGH
    case '#': output_ << "\n<li>"; break;
    case ';': 
      output_ << "\n<dt>";
      translate(text);
      output_ << "</dt>\n<dd>"; 
      break;
    default: break;
    }
  }
 
  void list(std::string text)
  {
    translate(text); 
  } 
 
  virtual void list_item_end(char type)           
  {
    switch (type)
    {
    case '*': // FALLTHROUGH  
    case '#': output_ << "</li>\n"; break;
    case ';': output_ << "</dd>\n"; break;
    default: break;
    }
  }
 
  void list_end(char type)
  {
    switch (type)
    {
    case '*': output_ << "</ul>\n";break;  
    case '#': output_ << "</ol>\n"; break;
    case ';': output_ << "</dl>\n"; break;
    default: break;
    }
  }
 
  void quote_start() 
  {
    output_ << "<blockquote class=\"quotation\">"; 
  }
 
  void quote(std::string text)       
  {
    translate(text);
    output_ << '\n'; 
  } 
 
  void quote_from_start() 
  {
    output_ << "<div class=\"quotedfrom\">";
  }
  void quote_from_end()   
  { 
    output_ << "</div>\n"; 
  }
  void quote_end()   
  { 
    output_ << "</blockquote>\n";
  }
 
  void document_end()              
  {
    output_ <<
      "\n"
      "<!-- Document ends here -->\n"
  //  "\n"
  //  "<a href=\"#top\">zur&uuml;ck zum Seitenanfang</a>\n"
      "</body></html>\n"; 
  }
 
  virtual void indentation_start()
  {
    output_ << "\n<div class=\"pseudocode\">\n";         
  }
 
  virtual void indentation_end()
  {
    output_ << "\n</div>\n";         
  }
 
  void indent(int level, bool firstline)
  {
    if (!firstline) output_ << "\n<br>";
    while (level-->0)
    {
      output_ << "&nbsp;&nbsp;";
    }
  }
 
  void translate(std::string text)
  { 
    std::string::iterator iter = text.begin(); 
    while (iter != text.end())
    {
      switch (*iter)
      {
      case '<'   : output_ << "&lt;"; break;  
      case '>'   : output_ << "&gt;"; break;  
      case '&'   : output_ << "&amp;"; break;  
 
      //  german Umlauts, ISO-8859-1 (ISO-Latin1) assumed
      case '\xC4': output_ << "&Auml;"; break; 
      case '\xE4': output_ << "&auml;"; break; 
      case '\xD6': output_ << "&Ouml;"; break; 
      case '\xF6': output_ << "&ouml;"; break; 
      case '\xDC': output_ << "&Uuml;"; break; 
      case '\xFC': output_ << "&uuml;"; break; 
      case '\xDF': output_ << "&szlig;"; break; 
 
      default: output_ << *iter; break;  
      }
      ++iter;
    }  
  }
};
#endif // HTMLTARGET_HPP
//~

Für Testzwecke kann eine Mini-Dokumentation aus einer Zeichenkette erzeugt werden.

// #define HTMLTEST
#ifdef  HTMLTEST
 
//: htmltst.cpp : dokgen Test HTMLtarget - R.Richter 2002-11-21
/////////////////////////////////////////////////////////////////////
 
#include <fstream>
#include <sstream>      // noch unbekannt in Dev C++, g++ 
#ifndef ALL_IN_ONE
  #include "parser.hpp"
  #include "htmltarget.hpp"
#endif // ALL_IN_ONE
 
int main() 
{
  std::string teststring =
  "/// @title dokgen : ein Dokumentationsgenerator\n"
  "/// @author René Richter\n"
  "/// @keywords Dokumentation, automatisiert\n"
  "\n"
  "/// > Willst du dir ein @code{Juwel} schleifen,\\\n"
  "/// > solltest du wenigstens mit @code{Rohdiamant} beginnen.\n"
  "/// >--- Steve McConnell\n"
  "/// System-Dokumentation: @link dokgen.htm {ausführlicher} Version: 2002-12-02.\n"
  "\n"
  "/// === Überblick\n"
  "/// DokGen ist ein Programmierwerkzeug, das es erleichtert,\n"
  "/// aktuelle Dokumente aus den Programmquellen zu erzeugen.\n"
  "\n"
  "/// @image onesrc.gif\n"
  "\n"
  "/// Die Erläuterungen sind im Quelltext integriert.\n"
  "/// Dokumentationszeilen beginnen mit genau drei Schrägstrichen am linken Rand.\n"
  "/// ==== Dokumentkopf\n"
  " /// @title dokgen : ein Dokumentationsgenerator\n"
  " /// @author René Richter\n"
  " /// @keywords Dokumentation, automatisiert\n"
  "/// Alle Angaben sind optional.\n"
  "/// === Überschriften\n"
  " /// === Abschnitt\n"
  " /// ==== Unterabschnitt\n"
  " /// ===== weitere ...\n"
  " /// ====== letzte (4.) Gliederungsebene\n"
 
  "/// ==== Textelemente\n"
  "/// Der Dokumentationstext ist strukturierbar mit\n"
  "/// * Listen\n"
  "/// *# ohne Nummern,\n"
  "/// *# mit Nummern,\n"
  "/// *# von Begriffsdefinitionen, z.B.\n"
  "/// *#; Begriff : erläuternder Text\n"
  "/// * Tabellen,\n"
  "/// * eingerücktem Text und\n"
  "/// * Zitaten.\n"
  "\n"
  "/// Listen beliebigen Typs sind schachtelbar:\n"
  " /// * Listen\n"
  " /// *# ohne Nummern,\n"
  " /// *# mit Nummern,\n"
  " /// *# von Begriffsdefinitionen, z.B.\n"
  " /// *#; Begriff : erläuternder Text\n"
  "/// Mehrzeilige Listeneinträge sind in einen Klammerblock einzuschließen.\n"
  "\n"
  "/// Tabellenzellen beginnen immer mit einem senkrechten Strich.\n"
  "/// Optional kann dem Strich eine Ausrichtungspfeil folgen.\n"
  "/// | @code{|<} | linksbündig \n"
  "/// | @code{|^} | zentriert \n"
  "/// | @code{|>} | rechtsbündig. \n"
  "/// | ohne      | Standardausrichtung\n"
  " /// | @code{|<} | linksbündig \n"
  " /// | @code{|^} | zentriert \n"
  " /// | @code{|>} | rechtsbündig. \n"
  " /// | ohne Pfeil| Standardausrichtung\n"
  "\n"
  "/// Eingerückter Text eignet sich besonders für Algorithmenbeschreibungen\n"
  "/// (Pseudocode):\n"
  "/// : für alle Dokumentzeilen\n"
  "/// :: verarbeiten\n"
  " /// : für alle Dokumentzeilen\n"
  " /// :: verarbeiten\n"
  "/// Zitate beginnen mit einem Zeichen @code|>| (wie in Mailsystemen).\n"
  "/// Die Herkunft kann nach @code|>---| angegeben werden:\n"
  " /// > Suddenly, documentation isn't so bad.\n"
  " /// >--- Andrew Hunt, David Thomas: The Pragmatic Programmer\n"
  "/// > Suddenly, documentation isn't so bad.\n"
  "/// >--- Andrew Hunt, David Thomas: The Pragmatic Programmer\n"
  "\n"
  "/// ==== Marken im Fließtext\n"
  "/// Im Dokumentabschnitten können Marken eingefügt werden.\n"
  "/// | @code{@code}  | Schlüsselwort im Text \n"
  "/// | @code{@image} | Bild\n"
  "/// | @code{@label} | Verweisziel innerhalb des Dokuments \n"
  "/// | @code{@link}  | Verweis außerhalb des Dokuments (Hypertextreferenz)\n"
  "/// | @code{@ref}   | Verweis innerhalb des Dokuments\n"
  "\n"
  "/// In @code|@code{...}|-Abschnitten dürfen\n"
  "/// keine geschweiften Klammern vorkommen.\n"
  "/// Es kann ein beliebiges anderes Zeichen\n"
  "/// als Begrenzer eingesetzt werden,\n"
  "/// das nicht innerhalb des Blocks vorkommt:\n"
  "///  @code+@code|{...}|+ erzeugt @code|{...}|. \n"
 
  "/// Bild- und Verweismarken folgt eine Bezeichnung ohne Leerzeichen.\n"
  "/// Optional kann ein geschweifter Klammerblock mit einem Text folgen\n"
  "/// (Bildunterschrift oder Beschreibung des Verweiszieles).\n"
  " /// Ausführliche Dokumentation siehe @link dokgen.htm {Anhang und Projektstatus}.\n"
  "/// Ausführliche Dokumentation siehe @link dokgen.htm {Anhang und Projektstatus}.\n"
  ;
 
  std::istringstream input(teststring);
  std::ofstream output("dokgen0.htm"); 
  Lexer lex(input);
  HTMLTarget target(output);
  Parser parser(lex, target);
  try 
  {
    parser.document();
  }
  catch(ParseError e)
  {
    std::cerr << e.what() << '\n';
  }
  return 0;
}
//~
#endif // HTMLTEST

DokuWiki-Ausgabe

Optional können DokuWiki-Quellen erzeugt werden.

//: dokuwikitarget.hpp : dokgen Basis Ausgabeziel - R.Richter 2011-04-27
////////////////////////////////////////////////////////////////////////
 
#ifndef DOKUWIKITARGET_HPP
#define DOKUWIKITARGET_HPP
 
#ifndef ALL_IN_ONE
  #include "target.hpp"
#endif // ALL_IN_ONE
 
class DokuWikiTarget : public Target
{
public:
  DokuWikiTarget(std::ostream& output)
  : Target(output)
  , listLevel_(0)
  {
  }
 
  void document_start() {}
 
  void make_title(std::string author, std::string title, std::string keywords) 
  {
    output_ << "====== " << title  << " ======\n"
            << "Autor: " << author << "\\\\\n"
                << "Schlagworte: " << keywords << '\n';
  }
 
  void paragraph_start() {}
  void paragraph_end() { output_ << "\n\n"; }
  void next_line() { output_ << "\\\\\n"; }
  void label(std::string text) {} 
 
  void link(std::string url, std::string caption)       
  {
    output_ << "[[" << url;
    if (caption != "") output_ << "|" << caption;
    output_ << "]]"; 
  }
  void image(std::string name, std::string caption)     
  {
    output_ << "{{" << name;
    if (caption != "") output_ << "|" << caption;
    output_ << "}}"; 
  }
  void ref(std::string label, std::string caption)      
  {
    output_ << "[[#" << label;
    if (caption != "") output_ << "|" << caption;
    output_ << "]]"; 
  }
 
  void section_start(std::string title) { output_ << "===== " << title << " =====\n"; }
  void subsection_start(std::string title) { output_ << "==== " << title << " ====\n"; }
  void subsubsection_start(std::string title) { output_ << "=== " << title << " ===\n"; }
  void subsubsubsection_start(std::string title) { output_ << "== " << title << " ==\n"; }
 
  void section_end(std::string title) {}
  void subsection_end(std::string title) {}
  void subsubsection_end(std::string title) {}
  void subsubsubsection_end(std::string title) {}
 
  void inlinecode(char begin, std::string text, char end)  
  {
    output_ << "\'\'" << text << "\'\'"; 
  }
 
  void code_start() { output_ << "<code cpp>\n"; }
  void code(std::string text) { output_ << text; } 
  void code_end() { output_ << "</""code>\n"; } // prevent Dokuwiki from interpreting
 
  void table_start() {}
  void table_cell_start(char alignment=' ') 
  {
    output_ << "|"; 
    switch (alignment)
    {
    case '>': output_ << " "; break;
    case '^': output_ << " "; break;
    default: break; 
    }
  }
  void table_cell_end() {}
  void table_row_start() {}
  void table_row_end() { output_ << "|\n"; }
  void table(std::string text) { output_ << text; } 
  void table_end() {}
 
  void list_start(char type)
  {
    switch (type)
    {
    case '*': // FALLTHROUGH
    case '#': 
    case ';': ++listLevel_; break;
    default : break;
    }
  }
 
  void list_item_start(char type, std::string text)           
  {
    output_ << std::string(2*listLevel_, ' ');
    switch (type)
    {
    case '*': output_ << "* "; break;
    case '#': output_ << "- "; break;
    case ';': 
      output_ << "* **" << text << "** : "; 
      break;
    default : break;
    }
  }
 
  void list(std::string text) 
  {
    output_ << text; 
  }
 
  void list_item_end(char type) {}
 
  void list_end(char type)                  
  {
    switch (type)
    {
    case '*': // FALLTHROUGH
    case '#':
    case ';': --listLevel_; break;
    default : break;
    }
  }
 
  void quote_start() { output_ << "<file>\n"; }
  void quote(std::string text) { output_ << text; } 
  void quote_from_start() { output_ << "--- "; }
  void quote_from_end() { output_ << ""; }
  void quote_end() { output_ << "</file>\n"; } 
 
  void indentation_start() {}
  void indentation_end() {}
  void indent(int depth = 1, bool firstline = false) 
  {
    output_ << std::string(depth, '>') << ' '; 
  }
  void document_end() {}
private:
  int listLevel_;
};
#endif // DOKUWIKITARGET_HPP
//~

Mehrzeilige Listeneinträge sind im Ausgabetext nachzubearbeiten, da diese von DokuWiki nicht unterstützt werden.

Hauptprogramm

Die Verarbeitung-Pipeline std::cin » lex » parser » target » std::cout wird zusammengesetzt und die Verarbeitung angestoßen. Bei Fehlern wird vom Parser eine Ausnahme ParseError geworfen, die abgefangen werden muss, um das Programm ordentlich zu beenden.

#define DOKGEN_MINIMAL
#ifdef  DOKGEN_MINIMAL
 
//: dokgen.cpp : Dokumentationsgenerator - R.Richter 2002-11-23
/////////////////////////////////////////////////////////////////////
 
#include <cstdlib>
#ifndef ALL_IN_ONE
  #include "parser.hpp"
  #include "htmltarget.hpp"
  #include "dokuwikitarget.hpp"
#endif // ALL_IN_ONE
 
int main(int argc, char* argv[]) 
{
  Lexer lex(std::cin);
  HTMLTarget html(std::cout);
  DokuWikiTarget dokuwiki(std::cout);
  Target* target = &html;
 
  if (argc == 2 && std::string(argv[1]) == "--dokuwiki") target = &dokuwiki;
 
  Parser parser(lex, *target);
  try 
  {
    parser.document();
  }
  catch(ParseError e)
  {
    std::cerr << e.what() << '\n';
    return EXIT_FAILURE;
  }
  return EXIT_SUCCESS;
}
//~
#endif // DOKGEN_MINIMAL

Anhang

Zur Vervollständigung des dokgen-Systems gehören die Werkzeuge dokstrip und extract. Über eine CSS-Datei docstyle.css lässt sich das Aussehen der entstandenen HTML-Seiten anpassen. Die Quelltexte der Programme sind als ZIP-Datei verfügbar. Nach Umbenennen der Endungen *.txt in *.cpp sollten die Quellen z.B. mit make dokgen übersetzbar sein.

Projektstatus

Status: Experimentell. Erzeugt gültigen HTML 4.01 Transitional Code.

Versionsgeschichte

Datum Version Beschreibung
2011-04-29 0.10 Erst-Veröffentlichung
2011-04-27 0.10 Target DokuWiki ergänzt
2011-04-26 0.10 Übersetzt / getestet mit g++ 4.6.0
2007-03-18 0.09 Bild onesrc.bmp → onesrc.gif, Versionsgeschichte
2007-03-17 0.09 W3C HTML 4.01 Trasnsitional Validierung bestanden
2007-03-17 0.09 <img> standalone Element, Tags in Kleinschreibung
2007-03-17 0.09 Syntaxänderung: paragraph ist textelement, document, section
2007-03-16 0.09 Syntaxänderung: verschachtelte Listen in listitem
2004-09-09 0.08 Neuübersetzung wegen Systemupdate (msvcrt.dll)
2002-12-09 0.07 setup.bat im Hauptverzeichnis
2002-12-09 0.06 dokgen-system und Testumgebung, make-Workflow
2002-12-08 0.05 Dokumentation Werkzeuge docstrip etc. umgestellt
2002-12-03 0.04 Syntaxänderung: Tabellen und Listen im Wiki-Stil
2002-11-26 0.03 mit HTML-Dokumentation
2002-11-25 0.02 Dokumentation ergänzt
2002-11-25 0.01 Projektstart, erste lauffähige Version

Fehler und Mängel

  • Option --dokuwiki: Mehrzeilige Listeneinträge sind für DokuWiki manuell nachzubearbeiten
anwenden/onesource/dokgen.txt · Zuletzt geändert: 2016-12-28 12:26 (Externe Bearbeitung)