Drupal Ontwikkeling eenvoudiger met de Qt Assistant - Deel 3a

Welkom terug! Het is een tijd geleden sinds ik deel een en twee van deze serie heb gepost (ja, ik moet vaker bloggen...). Weet je nog dat ik de Drupal documentatie in de Qt Assistant, een handige viewer voor documentatie, aan het zetten was? Mooi. Ik heb goed nieuws: ik heb weer wat tijd gestoken in het in orde krijgen van de documentatie. Dus we gaan gelijk door!

Plan voor vandaag

In deel een hebben we ons systeem voorbereid en de nodige tools klaar gemaakt voor het werk. In deel twee kregen we de documentatie daadwerkelijk in de Qt Assistant, alhoewel er nog layout problemen waren zoals je misschien wel gemerkt hebt.

Nadat we de veranderingen in de Drupal documentatie sinds de vorige keer weer opgepakt hebben, zoomen we nu in op (Drupal specifieke) problemen in de gegenereerde documentatie. Hierbij hoort vooral het terughalen van tekst die verloren gaat in het Doxygen proces, door de code vooraf te "preprocessen." Dit is subdeel A, binnenkort krijgen we ook de specifieke onderwerpen (zoals de Forms API referentie) aan de praat in deel 3B.

Veranderingen sinds de vorige keer

Sinds de vorige keer zijn er enkele dingen veranderd in de bronnen die we gebruiken. Ook al gebruik ik nu een andere Linux distributie, lijkt het erop dat de Doxygen templates een make-over hebben gehad: je documentatie ziet er nu nog beter uit met de nieuwste versie. Alles heeft een wat moderner jasje, wat ik goed vind. Je wilt denk ik je software (Doxygen en Qt) even bijwerken naar de nieuwste versies als je dat niet al gedaan hebt, gewoon voor de handigheid.

Een belangrijkere aanpassing is echter dat de Drupal Developer Documentatie op CVS niet meer de voorbeelden bevat. In plaats daarvan zijn ze vehruisd naar een eigen project. Je kunt een snapshot van de geschikte versie downloaden en in je developer subdirectory uitpakken (waar je Drupal Developer Documentatie staat). Een andere handige optie is om de voorbeelden ook van CVS te halen (zie "Checking out from the contributions repository" voor gedetailleerde instructies):

cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -d developer/examples contributions/modules/examples

Vergeet niet je developer documentatie ook bij te werken naar de laatste versie. Met name als je Drupal 6 gebruikt, wil je vast ook even de goede branch selecteren, als volgt (vanuit de Drupal directory, vervang -r DRUPAL-6--1 met -A voor de laatste HEAD versie):

cvs -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib update -d -P -r DRUPAL-6--1 developer

Gewoon voor de volledigheid, om een schone checkout te doen van al deze extra documentatie, gebruik je de volgende twee commando's:

cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -r DRUPAL-6--1 -d developer contributions/docs/developer
cvs -z6 -d:pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib checkout -r DRUPAL-6--1 -d developer/examples contributions/modules/examples

Gedaan? Goed zo, dan gaan we weer verder...

Is je iets opgevallen?

Als je de documentatie die de vorige keer gemaakt is hebt gebruikt, ben je misschien wat vervelende problemen tegen gekomen. We slaan de kapotte links op de hoofdpagina nu nog even over. Eerst gaan we ontbrekende tekst corrigeren. Kijk maar eens naar de documentatie van de t() functie (in includes/common.inc). Je kunt zien wat er mis is in de volgende screenshot:

Kapotte Documentatie

Zie je hoe het stukje @variable ontbreekt bij het tweede item, net als het % teken voor variable in het derde puntje? Een kijkje in de Doxygen fout-uitvoer leert ons dat Doxygen het @variable commando niet kent. Doxygen commando's worden vooraf gegaan door een '@' of '\' in Doxygen, maar de Drupal API module (die gebruikt wordt voor api.drupal.org) negeert vrolijk alle onbekende commando's. Doxygen niet, wat hier problemen veroorzaakt. Met het '%' teken vertellen we Doxygen in principe dat we het woord 'variable' niet automatisch willen linken. Ook hier doet de Drupal API module niks mee en deze print het vrolijk uit.

We gaan deze problemen in een paar stappen oplossen. Deel een: tijd voor wat PHP Regular Expressions magie!

Een eenvoudige preprocessor

Maak een nieuw tekstbestand aan met een leuke naam als perprocess-drupal-doxygen.php. Dat zegt ons precies wat het doet: Drupal bestanden preprocessen voor Doxygen; en het is een PHP script. Om te beginnen maken we alvast een basis klasse met behulp van de volgende code:

  1. #!/usr/bin/php
  2. <?php
  3. abstract class Preprocessor {
  4.   private $_filename = '';
  5.  
  6.   public function __construct ($filename) {
  7.     $this->_filename = $filename;
  8.   }
  9.  
  10.   public function process() {
  11.     $contents = file_get_contents($this->_filename);
  12.     // Convert Mac/Win line breaks to Unix format.
  13.     $contents = str_replace("\r\n", "\n", $contents);
  14.     $contents = str_replace("\r", "\n", $contents);
  15.  
  16.     return $this->doProcess($contents);
  17.   }
  18.  
  19.   protected abstract function doProcess($contents);
  20. }

Opmerking: ik heb in de code hier (en verderop) het meeste commentaar uit de code gehaald; onderaan kun je het volledige script downloaden, met al het commentaar.

Sla het bestand op en zorg de je "uitvoer" rechten hebt. Op Windows moet je misschien een bestands associatie als *.phpx instellen om te zorgen dat het door PHP uitgevoerd wordt, en het bestand vervolgens de juiste naam geven. Een andere optie is om een .cmd bestand te maken dat het PHP script uitvoert (en alle argumenten doorgeeft).

De lap tekst doet een paar dingen. De eerste regel zorgt dat het script uitgevoerd wordt door PHP. Verder is er een Preprocessor klasse, die een bestand leest en alle nieuwe regels naar een Unix formaat omzet, voor de eenvoudigheid (code geleend van de Drupal API module). Daarna roept het de (abstracte) doProcess() functie aan. Hier werken we aan in twee subklasses (waarvan de tweede in deel 3B gemaakt zal worden). Deze gaan daadwerkelijk iets nuttigs doen.

We voegen nu eerst de CodePreprocessor klasse toe, die alle codebestanden (.php, .module, .inc etc.) zal verwerken. Om te beginnen gaan we commentaar eruit vissen:

  1. class CodePreprocessor extends Preprocessor {
  2.  
  3.   public function __construct ($filename) {
  4.     Preprocessor::__construct($filename);
  5.   }
  6.  
  7.   protected function doProcess($contents) {
  8.     // Beyond Drupal's API module: we also work on blocks started with "/*!"
  9.     $contents = preg_replace_callback('@/\*[\*!](.*?)\*/@s',
  10.                                       array($this, 'processCommentBlock'),
  11.                                       $contents);
  12.     // And those with at least two lines of /// or //!
  13.     $contents = preg_replace_callback('@(//[/!])[^\\n]*\\n(\\1[^\\n]*\\n)+@s',
  14.                                       array($this, 'processCommentBlock'),
  15.                                       $contents);
  16.     // Return processed file contents
  17.     return $contents;
  18.   }
  19.  
  20.   private function processCommentBlock($matches) {
  21.     $contents = $matches[0];
  22.  
  23.     // ADD FUNCTION CALLS HERE LATER
  24.  
  25.     return $contents;
  26.   }

De doProcess implementatie hier zoekt Doxygen commentaren op. In tegenstelling tot Drupal's API module, nemen we ook blokken mee die beginnen met '/*!', net als blokken van tenminste twee regels beginnend met '///' of '//!'. Dit zijn allemaal blokken die Doxygen gaat doorlopen, dus wij doen hetzelfde. Op elk blok roepen we processCommentBlock aan. Deze functie krijgt de matches array van de reguliere expressie ($matches[0] bevat de gehele match), en verwacht een string retour. Vervolgens wordt het gevangen stuk tekst vervangen met deze waarde. Al met al een handige functie om onze verwerking in te zetten.

Onbekende commando's escapen

Nu wat echte verwerkingscode. Om Doxygen de '@' en '%' tekens te laten behouden in de uitvoer (om het eerste probleem op te lossen), moeten we ze escapen, oftewel een backslash ervoor zetten. We willen echter niet dat bestaande Doxygen commando's genegeerd worden. We kunnen tegelijkertijd ook een ander probleem oplossen: drupal_match_path() heeft documentatie met daarin "\r" en "\n". Drupal's API module verwerkt het "\n" commando niet, terwijl Doxygen de regel afbreekt. Ook zeurt Doxygen dat het "\r" commando niet bestaat, en gooit het weg. We gaan dus ook commando's beginnend met een backslash escapen, net als (geldige!) backslash commando's gevolgd door een enkele letter (\a, \n enz), zo lang deze niet al ge-escaped zijn. Dus: \r wordt \\r (en weer \r in de HTML uitvoer), maar iets als \\e scaped blijft hetzelfde.

Dus: tijd voor meer magie met reguliere expressies. Voeg een functie toe in deCodePreprocessor class, die begint met het opnoemen van alle bekende Doxygen commando's:

  1.   private function escapeUnknownCommands($contents) {
  2.     static $commandsArray = array(
  3.       'a', 'addindex', 'addtogroup', 'anchor', 'arg', 'attention', 'author', 'b', 'brief', 'bug',
  4.       'c', 'callgraph', 'callgraph', 'callergraph', 'category', 'class', 'code', 'cond',
  5.       'copybrief', 'copydetails', 'copydoc', 'date', 'def', 'defgroup', 'deprecated', 'details', 'dir',
  6.       'dontinclude', 'dot', 'dotfile', 'e', 'else', 'elseif', 'em', 'endcode', 'endcond', 'enddot',
  7.       'endhtmlonly', 'endif', 'endlatexonly', 'endlink', 'endmanonly', 'endmsc', 'endverbatim', 'endxmlonly',
  8.       'enum', 'example', 'exception', 'extends', 'file', 'fn', 'headerfile', 'hideinitializer', 'htmlinclude',
  9.       'htmlonly', 'if', 'ifnot', 'image', 'implements', 'include', 'includelineno', 'ingroup', 'internal',
  10.       'invariant', 'interface', 'latexonly', 'li', 'line', 'link', 'mainpage', 'manonly', 'memberof', 'msc',
  11.       'n', 'name', 'namespace', 'nosubgrouping', 'note', 'overload', 'p', 'package', 'page', 'paragraph',
  12.       'param', 'post', 'pre', 'private', 'privatesection', 'property', 'protected', 'protectedsection',
  13.       'public', 'publicsection', 'protocol', 'ref', 'relates', 'relatesalso', 'remarks', 'return', 'retval',
  14.       'sa', 'section', 'see', 'showinitializer', 'since', 'skip', 'skipline', 'struct', 'subpage',
  15.       'subsection', 'subsubsection', 'test', 'throw', 'todo', 'tparam', 'typedef', 'union', 'until', 'var',
  16.       'verbatim', 'verbinclude', 'version', 'warning', 'weakgroup', 'xmlonly', 'xrefitem',
  17.       'annotatedclasslist', 'classhierarchy', 'define', 'functionindex', 'header', 'headerfilelist',
  18.       'inherit', 'l', 'postheader',
  19.     );
  20.     static $backslashCommandsArray = array(
  21.       'addindex', 'addtogroup', 'anchor', 'arg', 'attention', 'author', 'brief', 'bug',
  22.       'callgraph', 'callgraph', 'callergraph', 'category', 'class', 'code', 'cond',
  23.       'copybrief', 'copydetails', 'copydoc', 'date', 'def', 'defgroup', 'deprecated', 'details', 'dir',
  24.       'dontinclude', 'dot', 'dotfile', 'else', 'elseif', 'em', 'endcode', 'endcond', 'enddot',
  25.       'endhtmlonly', 'endif', 'endlatexonly', 'endlink', 'endmanonly', 'endmsc', 'endverbatim', 'endxmlonly',
  26.       'enum', 'example', 'exception', 'extends', 'file', 'fn', 'headerfile', 'hideinitializer', 'htmlinclude',
  27.       'htmlonly', 'if', 'ifnot', 'image', 'implements', 'include', 'includelineno', 'ingroup', 'internal',
  28.       'invariant', 'interface', 'latexonly', 'li', 'line', 'link', 'mainpage', 'manonly', 'memberof', 'msc',
  29.       'name', 'namespace', 'nosubgrouping', 'note', 'overload', 'package', 'page', 'paragraph',
  30.       'param', 'post', 'pre', 'private', 'privatesection', 'property', 'protected', 'protectedsection',
  31.       'public', 'publicsection', 'protocol', 'ref', 'relates', 'relatesalso', 'remarks', 'return', 'retval',
  32.       'sa', 'section', 'see', 'showinitializer', 'since', 'skip', 'skipline', 'struct', 'subpage',
  33.       'subsection', 'subsubsection', 'test', 'throw', 'todo', 'tparam', 'typedef', 'union', 'until', 'var',
  34.       'verbatim', 'verbinclude', 'version', 'warning', 'weakgroup', 'xmlonly', 'xrefitem',
  35.       'annotatedclasslist', 'classhierarchy', 'define', 'functionindex', 'header', 'headerfilelist',
  36.       'inherit', 'postheader',
  37.     );
  38.     static $noWordCommands = array(
  39.       'f[\$\[\]\{\}]', '[\$@\\\\&~<>#%"]',
  40.     );
  41.   }

Nu gebruiken we deze lijsten om de belangrijkste reguliere expressies die we nodig hebben te maken: twee stuks om de commando's te escapen. Oh, en we vergeten ook niet om alle procent-tekens te escapen, als dat niet al gebeurt is. Tijd om de zojuist gemaakte functie uit te breiden:

  1.     static $backslashCommandRegex = NULL;
  2.     static $commandsRegex = NULL;
  3.     static $noWordCommandsRegex = NULL;
  4.  
  5.     if (!isset($backslashCommandRegex)) {
  6.       $commandsRegex = '/(^|(?<=\W))(?<!\\\\|@)@(?!' . implode('\W|', $commandsArray) . '\W|'
  7.                      . implode('|', $noWordCommands) . ')/';
  8.       $backslashCommandRegex = '/(^|(?<=\W))(?<!\\\\|@)\\\\(?!'
  9.                              . implode('\W|', $backslashCommandsArray) . '\W|'
  10.                              . implode('|', $noWordCommands) . ')/';
  11.     }
  12.  
  13.     // First replace all unknown backslash commands that occur before commands
  14.     $contents = preg_replace($backslashCommandRegex, '\\\\\\\\', $contents);
  15.     // And unknown '@' prefixed commands
  16.     $contents = preg_replace($commandsRegex, '\@', $contents);
  17.     // Escape all unescaped percentage characters
  18.     $contents = preg_replace('/(?<!\\\\|@)%/', '\\\\%', $contents);
  19.     return $contents;

De reguliere expressies zien er ingewikkeld uit (vooral omdat we zelf een hoop backslashes moeten escapen), maar de werking is zoals hierboven beschreven. De 'noWordCommands' is apart gehouden, omdat dan die commando's gevolgd met een letter intact blijven. Een normaal commando dat gevolgd wordt door extra letters, moet alsnog een backslash ervoor krijgen (bijvoorbeeld: @var moet zo blijven, maar @variable moet veranderen in \@variable).

Nu moeten we de nieuwe functie nog aanroepen. Plaats de volgende regel in de belangrijke processCommentBlock() functie zoals eerder beschreven, bij de duidelijk gemarkeerde opmerking:

$contents = $this->escapeUnknownCommands($contents);

Uitproberen

Voordat we de code kunnen gaan gebruiken, moet er nog een beetje toegevoegd worden om de preprocessor aan te sturen, onderaan het script (na alle klasses):

  1. // Default to processing stdin if no arguments are given
  2. if ($argc == 1) {
  3.   $argc = 2;
  4.   $argv[1] = '-';
  5. }
  6.  
  7. // Process all files in argument list
  8. for ($i = 1; $i < $argc; ++$i) {
  9.   $filename = $argv[$i];
  10.  
  11.   if ($filename == "-") {
  12.     $filename = "php://stdin";
  13.   }
  14.  
  15.   // Find out type of file (based on filename)
  16.   $processor = NULL;
  17.   $info = pathinfo($filename);
  18.   if (!empty($info['extension']) && in_array($info['extension'], array('html', 'htm', 'xhtml'))) {
  19.     // HTML Processing is for later...
  20.   }
  21.   else {
  22.     $processor = new CodePreprocessor($filename);
  23.   }
  24.  
  25.   // Process file
  26.   print $processor->process();
  27. }

Deze code maakt een CodePreprocessor instantie aan (later voegen we hier een apart item voor HTML bestanden aan toe). Alle bestanden die als argument mee worden gegeven, wordt de code verwerkt en de resultaten teruggestuurd. Standaard wordt er van de standaard input gelezen, om het script te kunnen hergebruiken buiten Doxygen, mocht dat ooit nodig zijn (Dat was vooral handig tijdens het debuggen van het hele script, zodat ik het op selecties of andere bestanden kon gebruiken, direct vanuit mijn IDE).

Laten we het script eens gaan uitproberen. Sla het op (ik gebruik hier /home/cheetah/public_html/drupal.api/preprocess-drupal-doxygen.php) en open de Doxygen wizard weer eens (zoals in deel twee). Verander de instelling INPUT_FILTER in het Input onderdeel zodat het naar je script wijst. Het is ook nuttig om de bestandsnaam toe te voegen aan de lijst van uitgesloten bestanden (voeg preprocess-drupal-doxygen.php aan de EXCLUDE instelling in het Input onderdeel toe), mocht je het script in je Drupal directory opslaan. Hierdoor krijg je de documentatie van dit script niet in je Drupal documentatie.

Doxywizard - INPUT_FILTER instelling
Doxywizard - EXCLUDE instelling

Als je nu Doxygen uitvoert en de documentatie weer bekijkt, zul je zien of alles gelukt is. Zo ja, dan zou de documentatie van t() er nu uit moeten zien zoals het online is. Mooi.

Gerepareerde Documentation
Kapotte en Gerepareerde Documentatie

Kapotte links

Iets anders dat je direct al opvalt is dat op de hoofdpagina van de documentatie niet alle links werken. De links naar groepen (Module system, Database abstraction layer, enzovoorts) werken prima, net als de voorbeeldmodules als je die in je documentatie hebt staan. De links naar de constanten, globale variabelen en de geavanceerdere onderwerpen werken echter geen van allen. Tijd om dat ook op te lossen, door de preprocessor uit te breiden.

We beginnen met de link naar alle constanten, of enums in Doxygen. We weten dat er een pagina is, maar die staat niet op /api/constants. We willen de globals_enum.html pagina gebruiken; dat is de pagina met alle define() elementen die in de code gevonden worden. Dus maken we er een normale anchor link van (oftewel een <a> tag):

  1.   private function makeAnchorLinks($contents, $links) {
  2.     foreach($links as $original => $new) {
  3.       $re = '/@link ' . str_replace('/', '\/', $original) . '(\/\S*)?\s(.*\S)\s*@endlink/';
  4.       $contents = preg_replace($re, '<a class="el" href="' . $new . '">\\2</a>', $contents);
  5.     }
  6.     return $contents;
  7.   }

De volgende functie zal links naar /api/globals gaan veranderen in een link die naar de globals.php documentatie verwijst. Dit bestand, in de extra developer documentatie, bevat gedocumenteerde versies van alle globale variabelen in Drupal zelf. Een andere optie is om met bovenstaande functie de link naar globals_vars.html te laten wijzen, waarin alle globale variabelen (ook die van modules) opgesomd worden. Als je daarvoor kiest is de volgende functie onnodig, tenzij je meer kapotte links vind die je wilt repareren.

  1.   private function replaceLinks($contents, $links) {
  2.     foreach($links as $original => $new) {
  3.       $re = '/@link ' . str_replace('/', '\/', $original) . '(\/\S*)?\s(.*\S)\s*@endlink/';
  4.       $contents = preg_replace($re, '@link ' . $new . ' \\2 @endlink', $contents);
  5.     }
  6.     return $contents;
  7.   }

Zet beide functies in de CodePreprocessor klasse. Roep ze als volgt aan, na de aanroep naar escapeUnknownCommands:

  1.       $contents = $this->makeAnchorLinks($contents, array(
  2.         '/api/constants' => 'globals_enum.html',
  3.         // '/api/globals' => 'globals_vars.html',
  4.       ));
  5.       $contents = $this->replaceLinks($contents, array(
  6.         '/api/globals' => 'globals.php',
  7.       ));

Zie je de regel commentaar? Dat is het alternatief voor de globale variabelen; verwijder of maak commentaar van de andere regel over /api/globals als je liever de global_vars.html pagina gebruikt (je kunt daar hoe dan ook bij via Files, dan Globals in de Doxygen navigatie). Merk ook op dat de code flexibel genoeg is om snel meer links toe te voegen. Ook worden langere URLs automatisch meegenomen, zolang het extra deel met een slash begint (bijv. /api/constants/7 wordt ook in zijn geheel aangepast, maar /api/constants_7 wordt als andere link gezien).

Nu nog de externe links (zoals de "Drupal Programming from an Object-Oriented Perspective" link op de hoofdpagina). De volgende functie haalt het gebruikte @link commando weg, en vervangt het met een HTML tag die door Doxygen verder met rust gelaten wordt.

Now to deal with any external links (like the "Drupal Programming from an Object-Oriented Perspective" link on the main page). The following function will remove the used @link command, and replace it with an HTML tag again.

  1.   private function replaceExternalLinks($contents) {
  2.     return preg_replace('/@link (([a-zA-Z]+:\/\/|mailto\:)\S*)\s+(.*\S)\s*@endlink/',
  3.                  '<a class="el" href="\\1">\\3</a>', $contents);
  4.   }

En de aanroep:

$contents = $this->replaceExternalLinks($contents);

Let wel dat elk protocol (http://, ftp:// enz) als extern gezien wordt. Het mailto: protocol wordt enigszins apart afgheandeld, omdat er geen twee slashes achter komen, maar verder is alles gelijk. De hele URL, tot de eerste spatie, wordt gezien als de daadwerkelijke URL om naar te refereren. De rest is de tekst die gelinked wordt. Als je een stapje verder wilt gaan, kun je ook code toevoegen om de URL als gelinkte tekst te laten zien, als er verder geen tekst aanwezig is (Doxygen zal dat voor je doen als je de @link en @endlink commando's weg filtert). Dat laat ik verder als oefening open.

Samenvatting

Dus... wauw. Deze post is wat langer geworden dan ik had verwacht. Ik heb een vernieuwd Doxyfile in de bijlagen gezet (met de ingevulde INPUT_FILTER en aangepaste EXCLUDE instellingen), net als de code uit deze post. De grootste problemen in de Doxygen code zijn nu opgelost, waarmee de documentatie van Drupal meer geschikt wordt voor Doxygen zelf. Allemaal werk dat ook nuttig is als je de Qt Assistant niet gebruikt; zodra je Doxygen in plaats van Drupal's API module gebruikt, wil je de problemen die hier beschreven zijn opgelost hebben.

Hierna volgt niet deel 4, maar 3b. De volgende stap is namelijk om te zorgen dat Doxygen de HTML pagina's met documentatie, zoals de Forms API referentie, gaat toevoegen. Dat zal sneller gaan nu we de basis klaar hebben.

Andere delen in deze serie:

AttachmentSize
Doxyfile.62.95 KB
preprocess-drupal-doxygen.php_.txt9.55 KB