[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 );
+}