From de830875ea59f9f4de80ec330fb6a71fd6270a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 3 Aug 2026 13:43:05 -0300 Subject: [PATCH 1/2] Trim doctype, namespaces, comments --- configure.php | 98 ++++++++++++++++----- docbook/docbookwsi.php | 193 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 docbook/docbookwsi.php diff --git a/configure.php b/configure.php index 68f2d4cbd1..d91dc6aa78 100755 --- a/configure.php +++ b/configure.php @@ -208,11 +208,10 @@ function print_dom_errors() } } -function print_xml_errors() +function print_xml_errors( bool $omitNoFile = false ) { global $ac; $report = $ac['LANG'] == 'en' || $ac['XPOINTER_REPORTING'] == 'yes'; - $output = ( $ac['STDERR_TO_STDOUT'] == 'yes' ) ? STDOUT : STDERR ; $errors = libxml_get_errors(); libxml_clear_errors(); @@ -220,9 +219,7 @@ function print_xml_errors() $filePrefix = "file:///"; $tempPrefix = realpath( __DIR__ . "/temp" ) . "/"; $rootPrefix = realpath( __DIR__ . "/.." ) . "/"; - - if ( count( $errors ) > 0 ) - fprintf( $output , "\n" ); + $firstBreak = false; foreach( $errors as $error ) { @@ -231,9 +228,18 @@ function print_xml_errors() $line = $error->line; $clmn = $error->column; + if ( $file == '' && $omitNoFile ) + continue; + if ( str_starts_with( $mssg , 'XPointer evaluation failed:' ) && ! $report ) continue; // Translations can omit these, to focus on fatal errors + if ( ! $firstBreak ) + { + print "\n"; + $firstBreak = true; + } + if ( str_starts_with( $file , $filePrefix ) ) $file = substr( $file , strlen( $filePrefix ) ); if ( str_starts_with( $file , $tempPrefix ) ) @@ -243,7 +249,7 @@ function print_xml_errors() $prefix = $error->level === LIBXML_ERR_FATAL ? "FATAL" : "error"; - fwrite( $output , "[$prefix $file {$line}:{$clmn}] {$mssg}\n" ); + print "[$prefix $file {$line}:{$clmn}] {$mssg}\n"; } } @@ -717,11 +723,43 @@ 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"; +} +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; - return $dom->load( $filename , $options ); + + // 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 && $firstLoad ) + { + print_xml_errors( true ); + xml_trim_first( $dom ); + } + return $ret; } function dom_saveload( DOMDocument $dom , string $filename = "" ) : string @@ -736,24 +774,36 @@ function dom_saveload( DOMDocument $dom , string $filename = "" ) : string return $filename; } -echo "Creating monolithic temp/manual.xml... "; -$dom = new DOMDocument(); - -if ( dom_load( $dom , __DIR__ . '/../en/manual.xml' , true ) ) +function xml_trim_first( DOMDocument $doc ) { - echo " done.\n"; - print_dom_errors(); - dom_saveload( $dom ); // correct file/line/column on error messages -} -else -{ - 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, as 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']; @@ -779,7 +829,7 @@ 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 ); 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 @@ + +/* +This script reads a RelaxNG XML file, and calculates all elements +that _not_ contain 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 , and also gather all 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 s, as they cannot influence + // if a parent element is trimmable or not, and so that any + // inside of a cannot be found by XPaths, while exploring + // the original 's 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 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 ); +} From 08fca2ecce57e3bce3666769b9323ddfbb51e44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 3 Aug 2026 13:55:52 -0300 Subject: [PATCH 2/2] Do not bother with non fixable warnings. --- configure.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure.php b/configure.php index d91dc6aa78..2f4525b8ac 100755 --- a/configure.php +++ b/configure.php @@ -208,7 +208,7 @@ function print_dom_errors() } } -function print_xml_errors( bool $omitNoFile = false ) +function print_xml_errors() { global $ac; $report = $ac['LANG'] == 'en' || $ac['XPOINTER_REPORTING'] == 'yes'; @@ -228,7 +228,7 @@ function print_xml_errors( bool $omitNoFile = false ) $line = $error->line; $clmn = $error->column; - if ( $file == '' && $omitNoFile ) + if ( $file == '' ) continue; if ( str_starts_with( $mssg , 'XPointer evaluation failed:' ) && ! $report ) @@ -756,7 +756,7 @@ function dom_load( DOMDocument $dom , string $filename , bool $firstLoad ) : boo $ret = $dom->load( $filename , $options ); if ( $ret && $firstLoad ) { - print_xml_errors( true ); + print_xml_errors(); xml_trim_first( $dom ); } return $ret;