Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 73 additions & 23 deletions configure.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,14 @@ function print_xml_errors()
{
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();

$filePrefix = "file:///";
$tempPrefix = realpath( __DIR__ . "/temp" ) . "/";
$rootPrefix = realpath( __DIR__ . "/.." ) . "/";

if ( count( $errors ) > 0 )
fprintf( $output , "\n" );
$firstBreak = false;

foreach( $errors as $error )
{
Expand All @@ -231,9 +228,18 @@ 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

if ( ! $firstBreak )
{
print "\n";
$firstBreak = true;
}

if ( str_starts_with( $file , $filePrefix ) )
$file = substr( $file , strlen( $filePrefix ) );
if ( str_starts_with( $file , $tempPrefix ) )
Expand All @@ -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";
}
}

Expand Down Expand Up @@ -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();
xml_trim_first( $dom );
}
return $ret;
}

function dom_saveload( DOMDocument $dom , string $filename = "" ) : string
Expand All @@ -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'];
Expand All @@ -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 );

Expand Down
193 changes: 193 additions & 0 deletions docbook/docbookwsi.php
Original file line number Diff line number Diff line change
@@ -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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What parameters did you use to test this and determine the list? Perhaps we should document that.

I tried it using this, and it seems to work fine.

php docbook/docbookwsi.php docbook/docbook-5.2.1/rng/docbook.rng .manual.xml

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