Verschachtelte, hierarchische UL / LI-Listen auf eine Ebene reduzieren. Flatten hierarchical LI-lists to linear list.

Freitag, 28. Februar 2014

Für ein web2print-Projekt, das verschachtelte HTML-Listen in Tags für InDesign umwandeln sollte, sind wir auf ein Problem gestossen: InDesign stellt verschachtelte Listen nicht wirklich durch verschachtelte Tag-Elemente dar, sondern man erstellt vorab für jede Hierarchie in der Bullet-Liste ein eigenes Absatzformat. Die Herausforderung lag also darin, aus dem HTML-Input per PHP ein XML-Output zu generieren, in dem die Tiefe der Listen-Verschachtelungen zu “flachen” (flattened) Absatzformaten mit Kennzeichnung der Tiefe umgewandelt werden.

Ein Beispiel macht die Idee deutlich:
Aus einem HTML-Dokument mit einer klassischen, verschachtelten Liste…

Das ist eine Liste:
<ul>
   <li>Kapitel 1</li>
   <li>Kapitel 2
      <ul>
         <li>Kapitel 2.1</li>
         <li>Kapitel 2.2
            <ul>
               <li>Kapitel 2.2.1</li>
            </ul>
         </li>
      </ul>
   </li>
   <li>Kapitel 3</li>
</ul>
Viel Spass damit!

…sollte etwas in dieser Art generiert werden:

Das ist eine Liste:
<p class="depth-0">Kapitel 1</p>
<p class="depth-0">Kapitel 2</p>
<p class="depth-1">Kapitel 2.1</p>
<p class="depth-1">Kapitel 2.2</p>
<p class="depth-2">Kapitel 2.2.1</p>
<p class="depth-0">Kapitel 3</p>
Viel Spass damit!

Die Lösung setzt phpQuery.php ein – eine (geniale) Adaption der (noch genialeren) jQuery-Bibliothek für PHP statt JavaScript. Die Klasse mit entsprechender Rekursion dazu sah in PHP so aus:

<?php
require_once('phpQuery.php');
class renderEngine {
 
   function flatten_nested_lists ( $txt ) {
      $nodes = phpQuery::newDocument( $txt );
      $this->flatten_nested_lists_recursion( $nodes );
      return $nodes->html();
   }
 
   function flatten_nested_lists_recursion ( $el, $depth = 0) {
      $arr = [];
      foreach ($el->children('ul') as $cnt_ul=>$ul) {
       $ul = pq($ul);
        $arr = [];
        foreach ($ul->children('li') as $cnt_li=>$li) {
        $li = pq($li);
        $li->attr('depth', $depth);
        $liClone = $li->clone();
        $liClone->find('ul')->remove();
        $str = trim($liClone->attr('depth').' - '.$liClone->html());
        $arr[] = '<p aid:pstyle="Bullet_Lv_'.$liClone->attr('depth').'">'.$str.'</p>';
        $arr = array_merge($arr, $this->flatten_nested_lists_recursion($li, $depth+1));
      }
      foreach ($arr as $n) {
        $ul->before($n);
      }
      $ul->remove();
    }
    $el->children('ul')->remove();
    return $arr;
   }
}

…und in Funktion dann so:

 
$Engine = new renderEngine();
$input = '...hier der HTML-Code mit ul/li rein...';
echo $Engine->flatten_nested_lists($input);