[DOC-CVS] [doc-base] master: Trim the XML output file (#321)

[email protected] (alfsb via GitHub)
Newsgroups php.doc.cvs
Message-ID <[email protected]>
Author: alfsb (alfsb)
Committer: GitHub (web-flow)
Pusher: alfsb
Date: 2026-08-21T11:38:09-03:00

Commit: https://github.com/php/doc-base/commit/0b07c43157c59bd3795814791dbe7368faf24b99
Raw diff: https://github.com/php/doc-base/commit/0b07c43157c59bd3795814791dbe7368faf24b99.diff

Trim the XML output file (#321)

* Trim doctype, namespaces, comments
* Do not bother with non fixable warnings.

Changed paths:
  A  docbook/docbookwsi.php
  M  configure.php


Diff:

diff --git a/configure.php b/configure.php
index 49cd55922d..00a73963d0 100755
--- a/configure.php
+++ b/configure.php
@@ -194,27 +194,10 @@ function find_file($file_array) // {{{
     return '';
 } // }}}
 
-function print_dom_errors()
-{
-    $errors = libxml_get_errors();
-    foreach( $errors as $error )
-    {
-        $file = $error->file;
-        $line = $error->line;
-        $clmn = $error->column;
-        $prefix = $error->level === LIBXML_ERR_FATAL ? "FATAL" : "error";
-        $message = rtrim( $error->message );
-
-        if ( $file != '' )
-            print "[$prefix $file {$line}:{$clmn}] {$message}\n";
-    }
-}
-
 function print_xml_errors()
 {
     global $ac;
     $report = is_single_language() || $ac['XPOINTER_REPORTING'] == 'yes';
-    $output = ( $ac['STDERR_TO_STDOUT'] == 'yes' ) ? STDOUT : STDERR ;
 
     $errors = libxml_get_errors();
     libxml_clear_errors();
@@ -222,9 +205,7 @@ function print_xml_errors()
     $filePrefix = "file:///";
     $tempPrefix = realpath( __DIR__ . "/temp" ) . "/";
     $rootPrefix = realpath( __DIR__ . "/.." ) . "/";
-
-    if ( count( $errors ) > 0 )
-        fprintf( $output , "\n" );
+    $firstBreak = "\n";
 
     foreach( $errors as $error )
     {
@@ -233,9 +214,15 @@ function print_xml_errors()
         $line = $error->line;
         $clmn = $error->column;
 
+        if ( $file == '' )
+            continue;
+
         if ( str_starts_with( $mssg , 'XPointer evaluation failed:' ) && ! $report )
             continue; // Translations can omit these, to focus on fatal errors
 
+        print $firstBreak;
+        $firstBreak = "";
+
         if ( str_starts_with( $file , $filePrefix ) )
             $file = substr( $file , strlen( $filePrefix ) );
         if ( str_starts_with( $file , $tempPrefix ) )
@@ -244,8 +231,7 @@ function print_xml_errors()
             $file = substr( $file , strlen( $rootPrefix ) );
 
         $prefix = $error->level === LIBXML_ERR_FATAL ? "FATAL" : "error";
-
-        fwrite( $output , "[$prefix $file {$line}:{$clmn}] {$mssg}\n" );
+        print "[$prefix $file {$line}:{$clmn}] {$mssg}\n";
     }
 }
 
@@ -724,13 +710,44 @@ function dtd_text_entities()
 }
 checkvalue($ac["GENERATE"]);
 
+echo "Creating monolithic temp/manual.xml... ";
+$dom = new DOMDocument();
+
+if ( dom_load( $dom , __DIR__ . '/../en/manual.xml' , true ) )
+{
+    dom_saveload( $dom ); // correct file/line/column on error messages
+    echo " done.\n";
+    print_xml_errors();
+}
+else
+{
+    echo "failed.\n";
+    print_xml_errors();
+    xml_broken_files_check();
+    errors_are_bad(1);
+}
+
 function dom_load( DOMDocument $dom , string $filename , bool $firstLoad ) : bool
 {
     $filename = realpath( $filename );
-    $options = LIBXML_NOENT | LIBXML_COMPACT | LIBXML_BIGLINES | LIBXML_PARSEHUGE;
+
+    // On the first load we cannot use LIBXML_NSCLEAN, because
+    // libxml drops all namespaces inside DTD entities.
+
+    $options = LIBXML_NOENT
+             | LIBXML_COMPACT
+             | LIBXML_BIGLINES
+             | LIBXML_PARSEHUGE;
+    if ( ! $firstLoad )
+        $options |= LIBXML_NSCLEAN;
+
     $ret = $dom->load( $filename , $options );
+
     if ( $ret )
         $dom->documentElement->setAttribute( 'xml:lang' , $GLOBALS['ac']["LANG"] );
+    if ( $ret && $firstLoad )
+        xml_trim( $dom );
+
     return $ret;
 }
 
@@ -746,24 +763,37 @@ function dom_saveload( DOMDocument $dom , string $filename = "" ) : string
     return $filename;
 }
 
-echo "Creating monolithic temp/manual.xml... ";
-$dom = new DOMDocument();
-
-if ( dom_load( $dom , __DIR__ . "/../{$ac['LANG_BASE_DIR']}/manual.xml" , true ) )
-{
-    echo " done.\n";
-    print_dom_errors();
-    dom_saveload( $dom ); // correct file/line/column on error messages
-}
-else
+function xml_trim( DOMDocument $doc )
 {
-    echo "failed.\n";
-    print_xml_errors();
-    individual_xml_broken_check();
-    errors_are_bad(1);
+    $xpath = new DOMXPath( $doc );
+    $dtdNode = null;
+    $dels = [];
+
+    // Save and remove DTD Document Type node, after all entity
+    // references are already expanded at this point.
+
+    foreach( $doc->childNodes as $node )
+        if ( $node->nodeType == XML_DOCUMENT_TYPE_NODE )
+            $dtdNode = $node;
+
+    if ( $dtdNode != null )
+    {
+        $contents = $doc->saveXML( $dtdNode );
+        file_put_contents( __DIR__ . '/temp/doctype.dtd' , $contents );
+        $node->parentNode->removeChild( $dtdNode );
+    }
+
+    // Remove all XML comments, in reverse order, outside enumeration.
+
+    $comments = $xpath->query( "//comment()" );
+    for ( $idx = $comments->length - 1 ; $idx >= 0 ; $idx-- )
+    {
+        $node = $comments[ $idx ];
+        $node->parentNode->removeChild( $node );
+    }
 }
 
-function individual_xml_broken_check()
+function xml_broken_files_check()
 {
     $cmd = array();
     $cmd[] = $GLOBALS['ac']['PHP'];
@@ -781,7 +811,7 @@ function individual_xml_broken_check()
     }
 }
 
-xinclude_no_fallback( $dom );
+xinclude_remove_fallback( $dom );
 
 echo "Expanding XIncludes... ";
 $total  = xinclude_run_byid( $dom );
@@ -789,11 +819,11 @@ function individual_xml_broken_check()
 if ( $total == 0 )
     echo "failed.\n";
 else
-    echo "done: $total tags replaced.\n";
+    echo "done: $total tags.\n";
 
 xinclude_residual_fixup( $dom );
 
-function xinclude_no_fallback( DOMDocument $dom )
+function xinclude_remove_fallback( DOMDocument $dom )
 {
     // Check if there is reachable <xi:fallback>s.
     // Not permited in doc-en, warning on translations.
@@ -831,9 +861,8 @@ function xinclude_run_byid( DOMDocument $dom )
     // Avoids quadratic tree walks (~ 90% performance gain).
 
     $byId = [];
-    foreach( $xpath->query( "//*[@xml:id]" ) as $node ) {
+    foreach( $xpath->query( "//*[@xml:id]" ) as $node )
         $byId[$node->getAttribute("xml:id")] ??= $node;
-    }
 
     for( $run = 0 ; $run < 10 ; $run++ )
     {
diff --git a/docbook/docbookwsi.php b/docbook/docbookwsi.php
new file mode 100644
index 0000000000..c300b3f896
--- /dev/null
+++ b/docbook/docbookwsi.php
@@ -0,0 +1,193 @@
+<?php
+// SPDX-License-Identifier: 0BSD
+// © André L F S Bacci <ae#php.net>
+/*
+This script reads a RelaxNG XML file, and calculates all elements
+that _not_ contain <text/> contents, in all alternatives. That is,
+the list shows all elements that can have all inter-element
+whitespace removed, without affecting the expected parsing of the
+XML document that follows the RelaxNG specification.
+
+If run with a second XML argument, the script will calculate all savings
+that can be done by stripping these insignificant whitespace between
+elements.
+
+See mentions of 'docbookwsi' on source code for parts that need to be
+updated if/when a new version of Docbook is used.                */
+
+$argv0 = array_shift( $argv ) ?? null;
+$rngFile = array_shift( $argv ) ?? null;
+$xmlFile = array_shift( $argv ) ?? null;
+
+if ( $rngFile == null )
+{
+    print "Usage: '$argv0' rngFile [xmlFile]\n\n";
+    return;
+}
+
+$list = generate_element_trim_list( $rngFile );
+
+if ( $xmlFile == null )
+{
+    foreach( $list as $elem => $hasText )
+        if ( ! $hasText )
+            print "$elem\n";
+    exit( 0 );
+}
+else
+    xml_trim_stats( $xmlFile , $list );
+
+exit( 0 );
+
+function generate_element_trim_list( string $rngFilename ) : array
+{
+    $doc = new DOMDocument();
+    if ( ! $doc->load( $rngFilename , LIBXML_NOBLANKS ) )
+        throw new Exception( "XML load failed.\n" );
+
+    // First, we get all elements definitions that directly
+    // mentions <text/>, and also gather all <ref>s they refer.
+
+    $elemText = [];
+    $elemRefs = [];
+
+    $xpath1 = new DOMXpath( $doc );
+    $xpath2 = new DOMXpath( $doc );
+    $xpath1->registerNamespace ( 'rng' , 'http://relaxng.org/ns/structure/1.0' );
+    $xpath2->registerNamespace ( 'rng' , 'http://relaxng.org/ns/structure/1.0' );
+
+    $list = $xpath1->query( '//rng:element' );
+    foreach( $list as $elem )
+    {
+        $name = $elem->getAttribute( 'name' );
+        if ( $name == '' )
+            continue;
+
+        $text = count ( $xpath2->query( './/rng:text' , $elem ) );
+        $refs = $xpath2->query( './/rng:ref' , $elem );
+
+        $elemText[ $name ] = $text;
+        $elemRefs[ $name ] = [];
+
+        foreach( $refs as $ref )
+        {
+            $refName = $ref->getAttribute( 'name' );
+            $elemRefs[ $name ][] = $refName;
+        }
+    }
+
+    unset( $xpath1 );
+    unset( $xpath2 );
+
+    // After all elements are collected, and directly textual elements
+    // are marked, we can remove all <element>s, as they cannot influence
+    // if a parent element is trimmable or not, and so that any <text/>
+    // inside of a <element> cannot be found by XPaths, while exploring
+    // the original <element>'s <ref>erences.
+
+    $xpath3 = new DOMXpath( $doc );
+    $xpath3->registerNamespace ( 'rng' , 'http://relaxng.org/ns/structure/1.0' );
+
+    $todoDels = [];
+    $dels = $xpath3->query( '//rng:element' );
+    foreach( $dels as $del )
+        array_push( $todoDels , $del );
+    foreach( $todoDels as $del )
+        $del->parentNode->removeChild( $del );
+
+    // Then, we explore all references of all elements, for
+    // indirect mentions of <text/>s.
+
+    foreach( $elemText as $name => $text )
+    {
+        $text = element_references_contains_text( $doc , $name , $elemRefs[ $name ] );
+        $elemText[ $name ] |= $text;
+    }
+
+    return $elemText;
+}
+
+function element_references_contains_text( DOMDocument $doc , string $elemName , array $refs ) : bool
+{
+    $ret = false;
+    $doneRefs = [];
+    $todoRefs = array_unique( $refs );
+
+    $xpath = new DOMXpath( $doc );
+    $xpath->registerNamespace ( 'rng' , 'http://relaxng.org/ns/structure/1.0' );
+
+    while ( ( $refName = array_pop( $todoRefs ) ) != null )
+    {
+        $doneRefs[ $refName ] = true;
+
+        $defs = $xpath->query( "//rng:define[@name='$refName']" );
+        if ( $defs->count() != 1 )
+            throw new Exception( "Unique define search failed for '$refName'." );
+        $def = $defs[0];
+
+        $text = count ( $xpath->query( './/rng:text' , $def ) );
+        if ( $text )
+            return true;
+
+        $subRefs = $xpath->query( './/rng:ref' , $def );
+        foreach( $subRefs as $subRef )
+        {
+            $subRefName = $subRef->getAttribute( 'name' );
+            if ( isset( $doneRefs[ $subRefName ] ) )
+                continue;
+            $todoRefs[] = $subRefName;
+        }
+    }
+
+    return false;
+}
+
+function xml_trim_stats( string $xmlFilename , array $elemText )
+{
+    $doc = new DOMDocument();
+    if ( ! $doc->load( $xmlFilename ) )
+        throw new Exception( "XML load failed.\n" );
+
+    $stats = [];
+    xml_trim_stats_enter( $doc->documentElement , $elemText, $stats );
+    arsort( $stats );
+
+    $total = 0;
+    foreach( $stats as $elem => $trimSize )
+    {
+        print "$trimSize $elem\n";
+        $total += $trimSize;
+    }
+    print "\ntotal $total\n";
+}
+
+function xml_trim_stats_enter( DOMNode $node , array $elemText , array & $stats , int $level = 0 )
+{
+    $name = $node->nodeName;
+    $text = $elemText[ $name ] ?? true;
+
+    if ( ! $text )
+    {
+        $size = 0;
+        $dels = [];
+
+        foreach( $node->childNodes as $child )
+            if ( $child->nodeType == XML_TEXT_NODE )
+                if ( trim( $child->nodeValue ) == '' )
+                    $dels[] = $child;
+
+        foreach( $dels as $del )
+        {
+            $size += strlen( $del->nodeValue );
+            $del->parentNode->removeChild( $del );
+        }
+
+        if ( isset( $stats[ $name ] ) )
+            $stats[ $name ] += $size;
+        else
+            $stats[ $name ] = $size;
+    }
+
+    foreach( $node->childNodes as $child )
+        xml_trim_stats_enter( $child , $elemText , $stats , $level + 1 );
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.