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
124 changes: 108 additions & 16 deletions configure.php
Original file line number Diff line number Diff line change
Expand Up @@ -652,11 +652,43 @@ function dtd_text_entities()
}
checkvalue($ac["GENERATE"]);

function dom_load( DOMDocument $dom , string $filename , string $baseURI = "" ) : bool
echo "Creating monolithic temp/manual.xml... ";
$dom = new DOMDocument();

if ( dom_load( $dom , "{$ac['srcdir']}/{$ac["INPUT_FILENAME"]}" ) )
{
dom_saveload( $dom ); // correct file/line/column on error messages
echo " done.\n";
}
else
{
echo "failed.\n";
print_xml_errors();
xml_broken_file_check();
errors_are_bad(1);
}

function dom_load( DOMDocument $dom , string $filename , bool $firstLoad = true ) : 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 )
xml_trim_first( $dom );

return $ret;
}

function dom_saveload( DOMDocument $dom , string $filename = "" ) : string
Expand All @@ -666,28 +698,88 @@ function dom_saveload( DOMDocument $dom , string $filename = "" ) : string

libxml_clear_errors();
$dom->save( $filename );
dom_load( $dom , $filename );
dom_load( $dom , $filename , false );

return $filename;
}

echo "Creating monolithic temp/manual.xml... ";
$dom = new DOMDocument();

if ( dom_load( $dom , "{$ac['srcdir']}/{$ac["INPUT_FILENAME"]}" ) )
function xml_trim_first( DOMDocument $doc )
{
dom_saveload( $dom ); // correct file/line/column on error messages
echo " done.\n";
$xpath = new DOMXPath( $doc );
$dtdNode = null;
$dels = [];

// 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 )
$node->parentNode->removeChild( $dtdNode );

// Remove all XML comments, as they are distracting,
// in reverse order, outside enumerations.

$comments = $xpath->query( "//comment()" );
for ( $idx = $comments->length - 1 ; $idx >= 0 ; $idx-- )
{
$node = $comments[ $idx ];
$node->parentNode->removeChild( $node );
}

// See docbook/docbookwsi.php.

// These first five elements account for almost 50%
// of all insignificant Docbook whitespace in manual.

$trimList[] = 'refsect1';
$trimList[] = 'varlistentry';
$trimList[] = 'listitem';
$trimList[] = 'row';
$trimList[] = 'methodsynopsis';

// The first twenty elements account for almost 95%
// of all insignificant Docbook whitespace in manual.

$trimList[] = 'refentry';
$trimList[] = 'fieldsynopsis';
$trimList[] = 'variablelist';
$trimList[] = 'example';
$trimList[] = 'simplelist';
$trimList[] = 'refnamediv';
$trimList[] = 'tbody';
$trimList[] = 'reference';
$trimList[] = 'classsynopsis';
$trimList[] = 'section';
$trimList[] = 'tgroup';
$trimList[] = 'thead';
$trimList[] = 'itemizedlist';
$trimList[] = 'note';
$trimList[] = 'sect2';

xml_trim_docbook_wsi( $doc->documentElement , $trimList );
}
else

function xml_trim_docbook_wsi( DOMNode $parent , array $trimList )
{
echo "failed.\n";
print_xml_errors();
individual_xml_broken_check();
errors_are_bad(1);
$nodeList = $parent->childNodes;
for ( $idx = $nodeList->length - 1 ; $idx >= 0 ; $idx-- )
{
$node = $nodeList[ $idx ];
$type = $node->nodeType;

if ( $type == 1 ) // XML_ELEMENT_NODE
xml_trim_docbook_wsi( $node , $trimList );

if ( $type == 3 ) // XML_TEXT_NODE
if ( in_array( $parent->nodeName , $trimList ) )
if ( trim( $node->nodeValue ) == '' )
$parent->removeChild( $node );
}
}

function individual_xml_broken_check()
function xml_broken_file_check()
{
$cmd = array();
$cmd[] = $GLOBALS['ac']['PHP'];
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. */

$arg0 = 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 );
}