From c9762120451b9aad8127ba38d71d0a5a8d228415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 15 Jun 2026 19:56:44 -0300 Subject: [PATCH 01/11] Enable text entities on configure.php --- configure.php | 46 ++++++++++----- manual.xml | 2 +- scripts/entities.php | 115 ++++++++++++++++++++------------------ scripts/file-entities.php | 2 +- 4 files changed, 97 insertions(+), 68 deletions(-) diff --git a/configure.php b/configure.php index b8851a1280..4e4ec9aabe 100755 --- a/configure.php +++ b/configure.php @@ -551,10 +551,11 @@ function git_status() echo "\n" , trim( $output ) . "\n\n"; } -// DTD layer before first XML loading +// DTD entity layer before first XML loading dtd_conf_entities(); dtd_file_entities(); +dtd_text_entities(); function dtd_conf_entities() { @@ -564,22 +565,16 @@ function dtd_conf_entities() $conf[] = ""; - if ( $lang == 'en' ) - { - realpain( __DIR__ . "/temp/empty" , touch: true ); - $trans1 = realpain( __DIR__ . "/temp/empty" ); - $trans2 = realpain( __DIR__ . "/temp/empty" ); - $trans3 = realpain( __DIR__ . "/temp/empty" ); - } - else + if ( $lang != 'en' ) { $trans1 = realpain( __DIR__ . "/../$lang/language-defs.ent" ); $trans2 = realpain( __DIR__ . "/../$lang/language-snippets.ent" ); $trans3 = realpain( __DIR__ . "/../$lang/extensions.ent" ); + + $conf[] = ""; + $conf[] = ""; + $conf[] = ""; } - $conf[] = ""; - $conf[] = ""; - $conf[] = ""; if ( $ac['CHMENABLED'] == 'yes' ) { @@ -589,7 +584,7 @@ function dtd_conf_entities() else $conf[] = ""; - file_put_contents( __DIR__ . "/temp/manual.conf" , implode( "\n" , $conf ) ); + file_put_contents( __DIR__ . "/temp/manual.inc" , implode( "\n" , $conf ) ); } function dtd_file_entities() @@ -620,6 +615,31 @@ function dtd_file_entities() } } +function dtd_text_entities() +{ + global $ac; + $php = $ac['PHP']; + $lang = $ac["LANG"]; + + $parts = array(); + $parts[] = $php; + $parts[] = __DIR__ . "/scripts/entities.php"; + $parts[] = "en"; + if ( $lang != "en" ) + $parts[] = $lang; + + foreach ( $parts as & $part ) + $part = escapeshellarg( $part ); + $cmd = implode( ' ' , $parts ); + $ret = 0; + passthru( $cmd , $ret ); + + if ( $ret != 0 ) + { + echo "doc-base/scripts/entities.php FAILED.\n"; + exit( 1 ); + } +} checking("for if we should generate a simplified file"); if ($ac["GENERATE"] != "no") { diff --git a/manual.xml b/manual.xml index a5899fcd26..2c85e402bb 100644 --- a/manual.xml +++ b/manual.xml @@ -2,7 +2,7 @@ - + %configure; diff --git a/scripts/entities.php b/scripts/entities.php index 2e927c8cd7..d29209172d 100644 --- a/scripts/entities.php +++ b/scripts/entities.php @@ -12,18 +12,18 @@ +----------------------------------------------------------------------+ | Authors: André L F S Bacci | +----------------------------------------------------------------------+ -| Description: Collect individual entities into an .entities.ent file. | +| Description: Collect individual entities into an temp/entities.ent. | +----------------------------------------------------------------------+ # Mental model, or things that I would liked to know 20 years prior DTD Entity processing has more in common with DOMDocumentFragment than -DOMElement. In other words, simple text and multi rooted XML files -are valid contents, whereas they are not valid XML documents. +DOMElement. In other words, simple text and multi rooted XML fragments +are valid content, whereas they are not valid XML documents. Also, namespaces do not automatically "cross" between a parent document and their entities, even if they are included in the same -file, as local textual entities. s are, for all intended +file, as local textual entities. Each s are, for all intended purposes, separated documents, with separated namespaces and have *expected* different default namespaces. @@ -48,7 +48,7 @@ cases where global entities are being overwritten, or if expected translatable entities are missing translations. -# Individual XML Entities, or `.xml` files at `entities/` +# Individual XML Entity files, or `.xml` files at `doc-lang/entities/` As explained above, the individual entity contents are not really valid XML *documents*, they are only at most valid XML *fragments*. @@ -62,15 +62,15 @@ is *invalid* to place XML declaration in these fragment files, at least in files that are invalid XML documents (on multi-node rooted ones). -# Grouped entities files, file tracked +# Grouped XML Entity files For very small textual entities, down to simple text words or single tag elements that may never change, individual entity tracking is -an overkill. This script also loads grouped XML Entities files, at +an overkill. This script also loads grouped XML Entity files, at some expected locations, with specific semantics. These grouped files are really normal XML files, correctly annotated -with XML namespaces used on manuals, so any individual exported entity +with XML namespaces used on manual, so any individual exported entity has correct and clean XML namespace annotations. These grouped entity files are tracked normally by revcheck, but are not directly included in manual.xml.in, as they only participate in general entity loading, @@ -95,24 +95,21 @@ return; } -$filename = Entities::rotateOutputFile(); // idempotent +$filename = Entities::prepareOutputFile(); $langs = []; -$normal = true; $debug = false; for( $idx = 1 ; $idx < count( $argv ) ; $idx++ ) if ( $argv[$idx] == "--debug" ) - $normal = false; + $debug = true; else $langs[] = $argv[$idx]; -$debug = ! $normal; -if ( $normal ) - print "Creating .entities.ent..."; +if ( $debug ) + print "Running temp/entities.ent in debug mode.\n"; else - print "Creating .entities.ent in debug mode.\n"; -$debug = ! $normal; + print "Running temp/entities.ent..."; loadEnt( __DIR__ . "/../global.ent" , global: true , warnMissing: true ); foreach( $langs as $lang ) @@ -120,8 +117,8 @@ loadEnt( __DIR__ . "/../../$lang/global.ent" , global: true ); loadEnt( __DIR__ . "/../../$lang/manual.ent" , translate: true , warnMissing: true ); loadEnt( __DIR__ . "/../../$lang/remove.ent" , remove: true ); - loadDir( $langs , $lang ); - Entities::$debugUnique = false; + loadDir( $langs , $lang , $debug ); + Entities::$checkUnique = false; } Entities::writeOutputFile(); @@ -150,17 +147,16 @@ public function __construct( class Entities { - private static string $filename = __DIR__ . "/../temp/entities.ent"; // idempotent + private static string $filename = __DIR__ . "/../temp/entities.ent"; - private static array $entities = []; // All entities, bi duplications + private static array $entities = []; // All collected entities, no duplications private static array $global = []; // Entities expected not replaced private static array $replace = []; // Entities expected replaced / translated private static array $remove = []; // Entities expected not replaced and not used private static array $unique = []; // For detecting duplicated global+en entities private static array $count = []; // Name / Count - private static array $slow = []; // External entities, slow, uncontrolled file overwrites - public static bool $debugUnique = true; // Start on unique mode, disable on second language + public static bool $checkUnique = true; // Start on unique mode, disable on second language public static int $countUnstranslated = 0; public static int $countReplacedGlobal = 0; @@ -187,7 +183,7 @@ static function put( string $path , string $name , string $text , bool $global = else Entities::$count[$name]++; - if ( Entities::$debugUnique ) + if ( Entities::$checkUnique ) { if ( isset( Entities::$unique[ $name ] ) ) { @@ -200,14 +196,7 @@ static function put( string $path , string $name , string $text , bool $global = } } - static function slow( string $path ) - { - if ( isset( $slow[$path] ) ) - fwrite( STDERR , "Unexpected file overwrite: $path\n" ); - $slow[ $path ] = $path; - } - - static function rotateOutputFile() + static function prepareOutputFile() { if ( file_exists( Entities::$filename ) ) unlink( Entities::$filename ); @@ -292,18 +281,18 @@ function loadEnt( string $path , bool $global = false , bool $translate = false $text = rtrim( $text , "\n" ); $text = str_replace( "&" , "&" , $text ); + + // Remove XML declaration. $lines = explode( "\n" , $text ); - array_shift( $lines ); // remove XML declaration + array_shift( $lines ); $text = implode( "\n" , $lines ); Entities::put( $path , $name , $text , $global , $translate , $remove ); } } -function loadDir( array $langs , string $lang ) +function loadDir( array $langs , string $lang , bool $debug ) { - global $debug; - $dir = __DIR__ . "/../../$lang/entities"; $dir = realpath( $dir ); if ( $dir === false || ! is_dir( $dir ) ) @@ -376,36 +365,56 @@ function loadXml( string $path , string $text , bool $expectedReplaced ) function saveEntitiesFile( string $filename , array $entities ) { - $tmpDir = __DIR__ . "/temp"; // idempotent - $file = fopen( $filename , "w" ); fputs( $file , "\n\n\n" ); + $slowDir = __DIR__ . "/../temp/text-entities/"; + $slowFiles = []; + + if ( file_exists( $slowDir ) == false ) + mkdir( $slowDir , recursive: true ); + foreach( $entities as $name => $entity ) { $text = $entity->text; - $quote = ""; - // If the text contains mixed quoting, keeping it - // as an external file to avoid (re)quotation hell. + $quote = "'"; + $count = 0; - if ( strpos( $text , "'" ) === false ) - $quote = "'"; - if ( strpos( $text , '"' ) === false ) + if ( str_contains( $string , "'" ) ) + { $quote = '"'; - - if ( $quote == "" ) + $count++; + } + if ( str_contains( $string , '"' ) ) { - if ( $entity->path == "" ) - { - $entity->path = $tmpDir . "/{$entity->path}.tmp"; - file_put_contents( $entity->path , $text ); - } - fputs( $file , "path}'>\n\n" ); - Entities::slow( $entity->path ); + $quote = "'"; + $count++; } - else + + if ( $count < 2 ) + { + // Fast path for single or no quote: + // entity body directly quoted on output file. + fputs( $file , "\n\n" ); + continue; + } + + // Slow path: entity body as an external file, + // as to avoid (re)quotation hell. + + if ( $entity->path == "" ) + $entity->path = realpath( $tmpDir . "/{$entity->name}.ent" ); + + if ( file_exists( $entity->path ) ) + { + echo "\nDuplicated output file '{$entity->path}'.\n"; + exit( 1 ); + } + + fputs( $file , "path}'>\n\n" ); + file_put_contents( $entity->path , $text ); } fclose( $file ); diff --git a/scripts/file-entities.php b/scripts/file-entities.php index 9229b0b130..66b5ecf67c 100644 --- a/scripts/file-entities.php +++ b/scripts/file-entities.php @@ -118,7 +118,7 @@ writeEntity( $file , $ent ); fclose( $file ); -echo "done\n"; +echo "done.\n"; exit( 0 ); From 58e11138ce24bb56228cfafc6a85a8a811f9c8f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 15 Jun 2026 20:03:16 -0300 Subject: [PATCH 02/11] Cosmetics --- configure.php | 2 +- scripts/file-entities.php | 4 +++- scripts/{entities.php => text-entities.php} | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) rename scripts/{entities.php => text-entities.php} (99%) diff --git a/configure.php b/configure.php index 4e4ec9aabe..64c2353ead 100755 --- a/configure.php +++ b/configure.php @@ -623,7 +623,7 @@ function dtd_text_entities() $parts = array(); $parts[] = $php; - $parts[] = __DIR__ . "/scripts/entities.php"; + $parts[] = __DIR__ . "/scripts/text-entities.php"; $parts[] = "en"; if ( $lang != "en" ) $parts[] = $lang; diff --git a/scripts/file-entities.php b/scripts/file-entities.php index 66b5ecf67c..b6b277d86b 100644 --- a/scripts/file-entities.php +++ b/scripts/file-entities.php @@ -118,7 +118,9 @@ writeEntity( $file , $ent ); fclose( $file ); -echo "done.\n"; + +$total = count( $entities ); +echo "done: $total entities.\n"; exit( 0 ); diff --git a/scripts/entities.php b/scripts/text-entities.php similarity index 99% rename from scripts/entities.php rename to scripts/text-entities.php index d29209172d..a32e508d9e 100644 --- a/scripts/entities.php +++ b/scripts/text-entities.php @@ -107,9 +107,9 @@ $langs[] = $argv[$idx]; if ( $debug ) - print "Running temp/entities.ent in debug mode.\n"; + print "Running text-entities.ent in debug mode.\n"; else - print "Running temp/entities.ent..."; + print "Running text-entities.ent..."; loadEnt( __DIR__ . "/../global.ent" , global: true , warnMissing: true ); foreach( $langs as $lang ) From 754d47112bc14cd01ba1ec0af08f5b639fbcdc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 15 Jun 2026 20:18:47 -0300 Subject: [PATCH 03/11] Fix. --- scripts/text-entities.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/text-entities.php b/scripts/text-entities.php index a32e508d9e..bc0eb0eba9 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -376,17 +376,17 @@ function saveEntitiesFile( string $filename , array $entities ) foreach( $entities as $name => $entity ) { - $text = $entity->text; + $body = $entity->text; $quote = "'"; $count = 0; - if ( str_contains( $string , "'" ) ) + if ( str_contains( $body , "'" ) ) { $quote = '"'; $count++; } - if ( str_contains( $string , '"' ) ) + if ( str_contains( $body , '"' ) ) { $quote = "'"; $count++; @@ -397,7 +397,7 @@ function saveEntitiesFile( string $filename , array $entities ) // Fast path for single or no quote: // entity body directly quoted on output file. - fputs( $file , "\n\n" ); + fputs( $file , "\n\n" ); continue; } @@ -414,7 +414,7 @@ function saveEntitiesFile( string $filename , array $entities ) } fputs( $file , "path}'>\n\n" ); - file_put_contents( $entity->path , $text ); + file_put_contents( $entity->path , $body ); } fclose( $file ); From 62c944ee0ad028e374778f98a103d439a7b9f653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 15 Jun 2026 20:21:57 -0300 Subject: [PATCH 04/11] Fix comment --- scripts/text-entities.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/text-entities.php b/scripts/text-entities.php index bc0eb0eba9..8bd9e4a8a6 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -366,7 +366,7 @@ function loadXml( string $path , string $text , bool $expectedReplaced ) function saveEntitiesFile( string $filename , array $entities ) { $file = fopen( $filename , "w" ); - fputs( $file , "\n\n\n" ); + fputs( $file , "\n\n\n" ); $slowDir = __DIR__ . "/../temp/text-entities/"; $slowFiles = []; From e488bef9b405ef268f61ad60e4965daf86d64082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 15 Jun 2026 20:37:15 -0300 Subject: [PATCH 05/11] Idempotent filenames for individual/slow files --- scripts/text-entities.php | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/scripts/text-entities.php b/scripts/text-entities.php index 8bd9e4a8a6..7653faefbf 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -368,14 +368,14 @@ function saveEntitiesFile( string $filename , array $entities ) $file = fopen( $filename , "w" ); fputs( $file , "\n\n\n" ); - $slowDir = __DIR__ . "/../temp/text-entities/"; - $slowFiles = []; + $sepFileDir = __DIR__ . "/../temp/text-entities/"; - if ( file_exists( $slowDir ) == false ) - mkdir( $slowDir , recursive: true ); + if ( file_exists( $sepFileDir ) == false ) + mkdir( $sepFileDir , recursive: true ); foreach( $entities as $name => $entity ) { + $name = $entity->name; $body = $entity->text; $quote = "'"; @@ -404,17 +404,19 @@ function saveEntitiesFile( string $filename , array $entities ) // Slow path: entity body as an external file, // as to avoid (re)quotation hell. - if ( $entity->path == "" ) - $entity->path = realpath( $tmpDir . "/{$entity->name}.ent" ); + $path = $sepFileDir . "/{$entity->name}.ent"; - if ( file_exists( $entity->path ) ) + if ( file_exists( $path ) ) { - echo "\nDuplicated output file '{$entity->path}'.\n"; + echo "\nDuplicated text-entity file: '{$path}'.\n"; exit( 1 ); } - fputs( $file , "path}'>\n\n" ); - file_put_contents( $entity->path , $body ); + // realpath() only after file creation + + file_put_contents( $path , $body ); + $path = realpath( $path ); + fputs( $file , "\n\n" ); } fclose( $file ); From d5e2fb51da5225aba347d0d6273411fadeb9ea63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 15 Jun 2026 21:36:57 -0300 Subject: [PATCH 06/11] Does not show untranslated warnings with onlu one language --- scripts/text-entities.php | 53 ++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/scripts/text-entities.php b/scripts/text-entities.php index 7653faefbf..105325b87f 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -109,7 +109,7 @@ if ( $debug ) print "Running text-entities.ent in debug mode.\n"; else - print "Running text-entities.ent..."; + print "Running text-entities.ent... "; loadEnt( __DIR__ . "/../global.ent" , global: true , warnMissing: true ); foreach( $langs as $lang ) @@ -118,21 +118,18 @@ loadEnt( __DIR__ . "/../../$lang/manual.ent" , translate: true , warnMissing: true ); loadEnt( __DIR__ . "/../../$lang/remove.ent" , remove: true ); loadDir( $langs , $lang , $debug ); - Entities::$checkUnique = false; } Entities::writeOutputFile(); Entities::checkReplaces( $debug ); -echo " done: " , Entities::$countTotalGenerated , " entities"; +echo "done: " , Entities::$countTotalGenerated , " entities"; if ( Entities::$countUnstranslated > 0 ) echo ", " , Entities::$countUnstranslated , " untranslated"; if ( Entities::$countReplacedGlobal > 0 ) - echo ", " , Entities::$countReplacedGlobal , " global replaced"; + echo ", " , Entities::$countReplacedGlobal , " unique replaced"; if ( Entities::$countReplacedRemove > 0 ) echo ", " , Entities::$countReplacedRemove , " remove replaced"; -if ( Entities::$countDuplicated > 0 ) - echo ", " , Entities::$countDuplicated , " duplicated (first language)"; echo ".\n"; exit; @@ -156,20 +153,24 @@ class Entities private static array $unique = []; // For detecting duplicated global+en entities private static array $count = []; // Name / Count - public static bool $checkUnique = true; // Start on unique mode, disable on second language - public static int $countUnstranslated = 0; public static int $countReplacedGlobal = 0; public static int $countReplacedRemove = 0; public static int $countTotalGenerated = 0; - public static int $countDuplicated = 0; - static function put( string $path , string $name , string $text , bool $global = false , bool $replace = false , bool $remove = false ) + public static int $languagesCount = 0; // Controls untranslated checking + + static function put( string $path , string $name , string $text , bool $unique = false , bool $replace = false , bool $remove = false ) { $entity = new EntityData( $path , $name , $text ); Entities::$entities[ $name ] = $entity; - if ( $global ) + if ( ! isset( Entities::$count[ $name ] ) ) + Entities::$count[$name] = 1; + else + Entities::$count[$name]++; + + if ( $unique ) Entities::$global[ $name ] = $name; if ( $replace ) @@ -178,22 +179,9 @@ static function put( string $path , string $name , string $text , bool $global = if ( $remove ) Entities::$remove[ $name ] = $name; - if ( ! isset( Entities::$count[ $name ] ) ) - Entities::$count[$name] = 1; - else - Entities::$count[$name]++; - - if ( Entities::$checkUnique ) - { - if ( isset( Entities::$unique[ $name ] ) ) - { - Entities::$countDuplicated++; - if ( Entities::$countDuplicated == 1 ) - fwrite( STDERR , "\n\n" ); - fwrite( STDERR , " Duplicated entity: $name\n" ); - } - Entities::$unique[ $name ] = $entity; - } + if ( $unique && isset( Entities::$unique[ $name ] ) ) + fwrite( STDOUT , "\n Replaced unique entity: $name\n" ); + Entities::$unique[ $name ] = $name; } static function prepareOutputFile() @@ -219,6 +207,7 @@ static function checkReplaces( bool $debug ) foreach( Entities::$entities as $name => $text ) { $replaced = Entities::$count[$name] - 1; + $languages = Entities::$languagesCount; $expectedGlobal = in_array( $name , Entities::$global ); $expectedReplaced = in_array( $name , Entities::$replace ); $expectedRemoved = in_array( $name , Entities::$remove ); @@ -227,21 +216,21 @@ static function checkReplaces( bool $debug ) { Entities::$countReplacedGlobal++; if ( $debug ) - print "Expected global, replaced $replaced times: $name\n"; + print " Expected global, replaced $replaced times: $name\n"; } - if ( $expectedReplaced && $replaced != 1 ) + if ( $expectedReplaced && $replaced != 1 && $languages != 1 ) { Entities::$countUnstranslated++; if ( $debug ) - print "Expected translated, replaced $replaced times: $name\n"; + print " Expected translated, replaced $replaced times: $name\n"; } if ( $expectedRemoved && $replaced != 0 ) { Entities::$countReplacedRemove++; if ( $debug ) - print "Expected removed, replaced $replaced times: $name\n"; + print " Expected removed, replaced $replaced times: $name\n"; } } } @@ -322,6 +311,8 @@ function loadDir( array $langs , string $lang , bool $debug ) loadXml( $path , $text , $expectedReplaced ); } + + Entities::$languagesCount++; } function loadXml( string $path , string $text , bool $expectedReplaced ) From 8f9f1616336b7685664cdcdae1776e0e641f7be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Mon, 15 Jun 2026 22:06:20 -0300 Subject: [PATCH 07/11] Document entity files names/functions --- entities/global.ent-dist | 37 +++++++++++++++++++++++++++---------- entities/manual.ent-dist | 29 +++++++++++++++++++++++------ entities/remove.ent-dist | 38 +++++++++++++++++++++++++++----------- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/entities/global.ent-dist b/entities/global.ent-dist index a453871ca3..876083bb56 100644 --- a/entities/global.ent-dist +++ b/entities/global.ent-dist @@ -1,19 +1,36 @@ + - - + + + diff --git a/entities/manual.ent-dist b/entities/manual.ent-dist index d93f720ded..09562552f3 100644 --- a/entities/manual.ent-dist +++ b/entities/manual.ent-dist @@ -1,14 +1,31 @@ - - + + + diff --git a/entities/remove.ent-dist b/entities/remove.ent-dist index 18ae9e6288..feb87963da 100644 --- a/entities/remove.ent-dist +++ b/entities/remove.ent-dist @@ -1,20 +1,36 @@ + - - + + + From f4ffe4b831cc03498f0c41c7ca8cab23d0cd63e0 Mon Sep 17 00:00:00 2001 From: alfsb Date: Wed, 17 Jun 2026 09:06:40 -0300 Subject: [PATCH 08/11] Update scripts/text-entities.php Co-authored-by: Jordi Kroon --- scripts/text-entities.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/text-entities.php b/scripts/text-entities.php index 105325b87f..b205ab8d74 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -70,7 +70,7 @@ some expected locations, with specific semantics. These grouped files are really normal XML files, correctly annotated -with XML namespaces used on manual, so any individual exported entity +with XML namespaces used on the manual, so any individual exported entity has correct and clean XML namespace annotations. These grouped entity files are tracked normally by revcheck, but are not directly included in manual.xml.in, as they only participate in general entity loading, From 4c982fbd689a154a6ce8d80e8ead8ef751c098cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Wed, 17 Jun 2026 09:17:59 -0300 Subject: [PATCH 09/11] Review changes --- configure.php | 9 +++++---- scripts/text-entities.php | 16 ++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/configure.php b/configure.php index 64c2353ead..5ad9729337 100755 --- a/configure.php +++ b/configure.php @@ -621,10 +621,11 @@ function dtd_text_entities() $php = $ac['PHP']; $lang = $ac["LANG"]; - $parts = array(); - $parts[] = $php; - $parts[] = __DIR__ . "/scripts/text-entities.php"; - $parts[] = "en"; + $parts = + [ $php + , __DIR__ . "/scripts/text-entities.php" + , "en" + ]; if ( $lang != "en" ) $parts[] = $lang; diff --git a/scripts/text-entities.php b/scripts/text-entities.php index b205ab8d74..dca623995f 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -124,8 +124,8 @@ Entities::checkReplaces( $debug ); echo "done: " , Entities::$countTotalGenerated , " entities"; -if ( Entities::$countUnstranslated > 0 ) - echo ", " , Entities::$countUnstranslated , " untranslated"; +if ( Entities::$countUntranslated > 0 ) + echo ", " , Entities::$countUntranslated , " untranslated"; if ( Entities::$countReplacedGlobal > 0 ) echo ", " , Entities::$countReplacedGlobal , " unique replaced"; if ( Entities::$countReplacedRemove > 0 ) @@ -153,12 +153,12 @@ class Entities private static array $unique = []; // For detecting duplicated global+en entities private static array $count = []; // Name / Count - public static int $countUnstranslated = 0; + public static int $countLanguages = 0; // Controls untranslated checking + public static int $countUntranslated = 0; public static int $countReplacedGlobal = 0; public static int $countReplacedRemove = 0; public static int $countTotalGenerated = 0; - public static int $languagesCount = 0; // Controls untranslated checking static function put( string $path , string $name , string $text , bool $unique = false , bool $replace = false , bool $remove = false ) { @@ -200,14 +200,14 @@ static function writeOutputFile() static function checkReplaces( bool $debug ) { Entities::$countTotalGenerated = count( Entities::$entities ); - Entities::$countUnstranslated = 0; + Entities::$countUntranslated = 0; Entities::$countReplacedGlobal = 0; Entities::$countReplacedRemove = 0; foreach( Entities::$entities as $name => $text ) { $replaced = Entities::$count[$name] - 1; - $languages = Entities::$languagesCount; + $languages = Entities::$countLanguages; $expectedGlobal = in_array( $name , Entities::$global ); $expectedReplaced = in_array( $name , Entities::$replace ); $expectedRemoved = in_array( $name , Entities::$remove ); @@ -221,7 +221,7 @@ static function checkReplaces( bool $debug ) if ( $expectedReplaced && $replaced != 1 && $languages != 1 ) { - Entities::$countUnstranslated++; + Entities::$countUntranslated++; if ( $debug ) print " Expected translated, replaced $replaced times: $name\n"; } @@ -312,7 +312,7 @@ function loadDir( array $langs , string $lang , bool $debug ) loadXml( $path , $text , $expectedReplaced ); } - Entities::$languagesCount++; + Entities::$countLanguages++; } function loadXml( string $path , string $text , bool $expectedReplaced ) From c81df4d48467da5438c21bb058d98b84be6e953d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Wed, 17 Jun 2026 13:50:37 -0300 Subject: [PATCH 10/11] Rewrite in preparation for entity files on doc-lang/reference/ --- entities/{manual.ent-dist => normal.ent-dist} | 10 +- entities/remove.ent-dist | 10 +- entities/{global.ent-dist => unique.ent-dist} | 11 +- scripts/text-entities.php | 293 ++++++++++-------- 4 files changed, 178 insertions(+), 146 deletions(-) rename entities/{manual.ent-dist => normal.ent-dist} (72%) rename entities/{global.ent-dist => unique.ent-dist} (67%) diff --git a/entities/manual.ent-dist b/entities/normal.ent-dist similarity index 72% rename from entities/manual.ent-dist rename to entities/normal.ent-dist index 09562552f3..ca5b02df42 100644 --- a/entities/manual.ent-dist +++ b/entities/normal.ent-dist @@ -4,7 +4,7 @@ # Description -This is a "manual.ent" XML Entity file. +This is a "normal" XML Entity file. See doc-base/script/text-entities.php for details. Place here small entities that are expected to be translated. @@ -17,15 +17,15 @@ file at doc-lang/entities/entityname.xml. Single-, multi- or text rooted XML fragment, as long as it is - a well-balanced one. Mind the XML namespaces of - XML element below. + a well-balanced one. Mind the XML namespaces of the root XML + element below. --> - + xmlns:xlink = "http://www.w3.org/1999/xlink" + translate = "yes"> diff --git a/entities/remove.ent-dist b/entities/remove.ent-dist index feb87963da..01c45b673e 100644 --- a/entities/remove.ent-dist +++ b/entities/remove.ent-dist @@ -1,10 +1,10 @@ - + + xmlns:xlink = "http://www.w3.org/1999/xlink" + translate = "remove"> diff --git a/entities/global.ent-dist b/entities/unique.ent-dist similarity index 67% rename from entities/global.ent-dist rename to entities/unique.ent-dist index 876083bb56..145e36a724 100644 --- a/entities/global.ent-dist +++ b/entities/unique.ent-dist @@ -1,11 +1,11 @@ - + + xmlns:xlink = "http://www.w3.org/1999/xlink" + translate = "no"> diff --git a/scripts/text-entities.php b/scripts/text-entities.php index dca623995f..d94a14cd53 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -15,9 +15,10 @@ | Description: Collect individual entities into an temp/entities.ent. | +----------------------------------------------------------------------+ -# Mental model, or things that I would liked to know 20 years prior +# Mental model for DTD , + or things that I would liked to know 20 years ago -DTD Entity processing has more in common with DOMDocumentFragment than +DTD Entity contents have more in common with DOMDocumentFragment than DOMElement. In other words, simple text and multi rooted XML fragments are valid content, whereas they are not valid XML documents. @@ -41,11 +42,11 @@ doc-base/temp/entities.ent file with their respective DTD Entities. The output file has no duplications, so collection order is important -to keep the necessary operational semantics. Here, latter loaded entities +to create some operational semantics. Here, latter loaded entities takes priority (overrides) an previous defined one. Note that this is the reverse of DTD convention, where duplicated entity names are ignored. The priority order used here is important to allow detecting -cases where global entities are being overwritten, or if expected +cases where unique entities are being overwritten, or if expected translatable entities are missing translations. # Individual XML Entity files, or `.xml` files at `doc-lang/entities/` @@ -64,10 +65,10 @@ # Grouped XML Entity files -For very small textual entities, down to simple text words or single -tag elements that may never change, individual entity tracking is -an overkill. This script also loads grouped XML Entity files, at -some expected locations, with specific semantics. +For very small textual entities, down to simple text or single XML +elements that may never change, individual file tracking of entities +is an overkill. To avoid an infinitude of micro entity files, this script +also loads grouped XML Entity files, at some expected locations. These grouped files are really normal XML files, correctly annotated with XML namespaces used on the manual, so any individual exported entity @@ -76,10 +77,21 @@ in manual.xml.in, as they only participate in general entity loading, described above. -- global.ent - expected unreplaced -- manual.ent - expected replaced (translated) -- remove.ent - expected unused -- lang/entities/* - expected replaced (translated) +# Checks + +Groped XML Entity files are annotated with an attribute named "translate", +that accepts the following values: + +- "yes": these entities are expected to be translated or replaced; +- "no": these entities are expected not be translated or replaced; +- "delete": these entities should be deleted on sight. + +The characteristics above are validated at the end of the script. Use the +--debug argument to also list the names of misused entities. + +The "delete" value exists to make possible deleting entities from +doc-en while keeping translations building. To achieve this result, +move any recently deleted to a .ent file with translate="delete". */ @@ -89,51 +101,60 @@ ini_set( 'display_startup_errors' , 1 ); error_reporting( E_ALL ); -if ( count( $argv ) < 2 || in_array( '--help' , $argv ) || in_array( '-h' , $argv ) ) -{ - fwrite( STDERR , "\nUsage: {$argv[0]} [--debug] langCode [langCode]\n\n" ); - return; -} - -$filename = Entities::prepareOutputFile(); +Entities::truncateOutputFile(); $langs = []; $debug = false; +$usage = in_array( '--help' , $argv ) || in_array( '-h' , $argv ); -for( $idx = 1 ; $idx < count( $argv ) ; $idx++ ) - if ( $argv[$idx] == "--debug" ) +if ( count( $argv ) < 2 || $usage ) +{ + print "\nUsage: {$argv[0]} langCode [langCode] [--debug]\n\n"; + if ( $usage ) + exit( 0 ); + else + exit( 1 ); +} +array_shift( $argv ); +foreach( $argv as $arg ) + if ( $arg == "--debug" ) $debug = true; else - $langs[] = $argv[$idx]; + $langs[] = $arg; if ( $debug ) print "Running text-entities.ent in debug mode.\n"; else print "Running text-entities.ent... "; -loadEnt( __DIR__ . "/../global.ent" , global: true , warnMissing: true ); foreach( $langs as $lang ) { - loadEnt( __DIR__ . "/../../$lang/global.ent" , global: true ); - loadEnt( __DIR__ . "/../../$lang/manual.ent" , translate: true , warnMissing: true ); - loadEnt( __DIR__ . "/../../$lang/remove.ent" , remove: true ); - loadDir( $langs , $lang , $debug ); + loadDirEntities( $lang ); + loadDirReference( $lang ); + Entities::$countLanguages++; } Entities::writeOutputFile(); Entities::checkReplaces( $debug ); -echo "done: " , Entities::$countTotalGenerated , " entities"; +echo "done: generated " , Entities::$countTotalGenerated , " entities"; if ( Entities::$countUntranslated > 0 ) echo ", " , Entities::$countUntranslated , " untranslated"; -if ( Entities::$countReplacedGlobal > 0 ) - echo ", " , Entities::$countReplacedGlobal , " unique replaced"; -if ( Entities::$countReplacedRemove > 0 ) - echo ", " , Entities::$countReplacedRemove , " remove replaced"; +if ( Entities::$countUniqueReplaced > 0 ) + echo ", " , Entities::$countUniqueReplaced , " unique replaced"; +if ( Entities::$countRemoveReplaced > 0 ) + echo ", " , Entities::$countRemoveReplaced , " remove replaced"; echo ".\n"; exit; +enum EntityCheck +{ + case Unique; // Expected once + case Normal; // Expected used/translated + case Remove; // Expected unused +} + class EntityData { public function __construct( @@ -146,45 +167,34 @@ class Entities { private static string $filename = __DIR__ . "/../temp/entities.ent"; - private static array $entities = []; // All collected entities, no duplications - private static array $global = []; // Entities expected not replaced - private static array $replace = []; // Entities expected replaced / translated - private static array $remove = []; // Entities expected not replaced and not used - private static array $unique = []; // For detecting duplicated global+en entities - private static array $count = []; // Name / Count + private static array $merged = []; // All EntityData, merged by name, no duplications + private static array $unique = []; // Any entity marked unique + private static array $remove = []; // Any entity marked deleted + private static array $nameCount = []; // Name / Count - public static int $countLanguages = 0; // Controls untranslated checking + public static int $countLanguages = 0; // For translated check public static int $countUntranslated = 0; - public static int $countReplacedGlobal = 0; - public static int $countReplacedRemove = 0; + public static int $countUniqueReplaced = 0; + public static int $countRemoveReplaced = 0; public static int $countTotalGenerated = 0; - - static function put( string $path , string $name , string $text , bool $unique = false , bool $replace = false , bool $remove = false ) + static function put( string $path , string $name , string $text , bool $unique = false , bool $remove = false ) { $entity = new EntityData( $path , $name , $text ); - Entities::$entities[ $name ] = $entity; - - if ( ! isset( Entities::$count[ $name ] ) ) - Entities::$count[$name] = 1; - else - Entities::$count[$name]++; + Entities::$merged[ $name ] = $entity; if ( $unique ) - Entities::$global[ $name ] = $name; - - if ( $replace ) - Entities::$replace[ $name ] = $name; + Entities::$unique[ $name ] = $name; if ( $remove ) Entities::$remove[ $name ] = $name; - if ( $unique && isset( Entities::$unique[ $name ] ) ) - fwrite( STDOUT , "\n Replaced unique entity: $name\n" ); - Entities::$unique[ $name ] = $name; + if ( ! isset( Entities::$nameCount[ $name ] ) ) + Entities::$nameCount[ $name ] = 0; + Entities::$nameCount[ $name ]++; } - static function prepareOutputFile() + static function truncateOutputFile() { if ( file_exists( Entities::$filename ) ) unlink( Entities::$filename ); @@ -194,59 +204,92 @@ static function prepareOutputFile() static function writeOutputFile() { - saveEntitiesFile( Entities::$filename , Entities::$entities ); + outputFiles( Entities::$filename , Entities::$merged ); } static function checkReplaces( bool $debug ) { - Entities::$countTotalGenerated = count( Entities::$entities ); + Entities::$countTotalGenerated = count( Entities::$merged ); Entities::$countUntranslated = 0; - Entities::$countReplacedGlobal = 0; - Entities::$countReplacedRemove = 0; + Entities::$countUniqueReplaced = 0; + Entities::$countRemoveReplaced = 0; - foreach( Entities::$entities as $name => $text ) + foreach( Entities::$merged as $name => $null ) { - $replaced = Entities::$count[$name] - 1; + $replaced = Entities::$nameCount[$name] - 1; $languages = Entities::$countLanguages; - $expectedGlobal = in_array( $name , Entities::$global ); - $expectedReplaced = in_array( $name , Entities::$replace ); + $expectedUnique = in_array( $name , Entities::$unique ); $expectedRemoved = in_array( $name , Entities::$remove ); + $expectedTranslated = ! ( $expectedUnique || $expectedRemoved ); - if ( $expectedGlobal && $replaced != 0 ) + if ( $expectedUnique && $replaced != 0 ) { - Entities::$countReplacedGlobal++; + Entities::$countUniqueReplaced++; if ( $debug ) - print " Expected global, replaced $replaced times: $name\n"; + print " Expected unique, replaced $replaced times: $name\n"; } - if ( $expectedReplaced && $replaced != 1 && $languages != 1 ) + if ( $expectedRemoved && $replaced != 0 ) { - Entities::$countUntranslated++; + Entities::$countRemoveReplaced++; if ( $debug ) - print " Expected translated, replaced $replaced times: $name\n"; + print " Expected removed, replaced $replaced times: $name\n"; } - if ( $expectedRemoved && $replaced != 0 ) + if ( $expectedTranslated && $replaced != 1 && $languages != 1 ) { - Entities::$countReplacedRemove++; + Entities::$countUntranslated++; if ( $debug ) - print " Expected removed, replaced $replaced times: $name\n"; + print " Expected translated, replaced $replaced times: $name\n"; } } } } -function loadEnt( string $path , bool $global = false , bool $translate = false , bool $remove = false , bool $warnMissing = false ) +function loadDirEntities( string $lang ) { - $realpath = realpath( $path ); - if ( $realpath === false ) + $dir = __DIR__ . "/../../$lang/entities"; + $dir = realpath( $dir ); + if ( $dir === false || ! is_dir( $dir ) ) + { if ( PARTIAL_IMPL ) + { + print "\n Skiped $lang/entities\n"; return; + } else - if ( $warnMissing ) - fwrite( STDERR , "\n Missing entity file: $path\n" ); - $path = $realpath; + { + print "\n Not a directory: $dir\n"; + exit( 1 ); + } + } + + $files = scandir( $dir ); + foreach( $files as $file ) + { + $path = realpath( "$dir/$file" ); + + if ( str_starts_with( $file , '.' ) ) + continue; + if ( is_dir( $path ) ) + continue; + + if ( str_ends_with( $path , ".xml" ) ) + loadEntitySingle( $path ); + + if ( str_ends_with( $path , ".ent" ) ) + loadEntityGroup( $path ); + } +} + +function loadDirReference( string $lang ) +{ + // TODO +} +function loadEntityGroup( string $path ) +{ + $path = realpath( $path ); $text = file_get_contents( $path ); $text = str_replace( "&" , "&" , $text ); @@ -254,12 +297,29 @@ function loadEnt( string $path , bool $global = false , bool $translate = false if ( ! $dom->loadXML( $text ) ) die( "XML load failed for $path\n" ); + $unique = false; + $remove = false; + $value = $dom->documentElement->getAttribute("translate"); + switch ( $value ) + { + case "no": + $unique = true; + break; + case "delete": + case "remove": + $remove = true; + break; + default: + print "\n Invalid translate value '$value' in '$path'.\n"; + exit( 1 ); + } + $xpath = new DOMXPath( $dom ); $list = $xpath->query( "/*/*" ); foreach( $list as $ent ) { - // weird, namespace correting, DOMNodeList -> DOMDocumentFragment transform + // Weird, namespace correting, DOMNodeList -> DOMDocumentFragment transform $other = new DOMDocument( '1.0' , 'utf8' ); foreach( $ent->childNodes as $node ) @@ -267,97 +327,68 @@ function loadEnt( string $path , bool $global = false , bool $translate = false $name = $ent->getAttribute( "name" ); $text = $other->saveXML(); - - $text = rtrim( $text , "\n" ); $text = str_replace( "&" , "&" , $text ); - // Remove XML declaration. + // Remove XML declaration and empty line added by libxml + $lines = explode( "\n" , $text ); array_shift( $lines ); + array_pop( $lines ); $text = implode( "\n" , $lines ); - Entities::put( $path , $name , $text , $global , $translate , $remove ); - } -} - -function loadDir( array $langs , string $lang , bool $debug ) -{ - $dir = __DIR__ . "/../../$lang/entities"; - $dir = realpath( $dir ); - if ( $dir === false || ! is_dir( $dir ) ) - if ( PARTIAL_IMPL ) - { - if ( $debug ) - print "Not a directory: $dir\n"; - return; - } - else - exit( "Error: not a directory: $dir\n" ); - - $files = scandir( $dir ); - $expectedReplaced = array_search( $lang , $langs ) > 0; - - foreach( $files as $file ) - { - $path = realpath( "$dir/$file" ); - - if ( str_starts_with( $file , '.' ) ) - continue; - if ( is_dir( $path ) ) - continue; - - $text = file_get_contents( $path ); - $text = rtrim( $text , "\n" ); - - loadXml( $path , $text , $expectedReplaced ); + Entities::put( $path , $name , $text , $unique , $remove ); } - - Entities::$countLanguages++; } -function loadXml( string $path , string $text , bool $expectedReplaced ) +function loadEntitySingle( string $path ) { + $text = file_get_contents( $path ); $info = pathinfo( $path ); $name = $info["filename"]; $frag = "$text"; if ( trim( $text ) == "" ) { - if ( ! PARTIAL_IMPL ) - fwrite( STDERR , "\n Empty entity (should it be in remove.ent?): '$path' \n" ); + print "\n Empty entity '$name' on file '$path'.\n"; + print "\n Should it be in a file with translate='remove'?\n"; Entities::put( $path , $name , $text ); return; } + // Validate. Accepts only the error "Entity * not defined" + $dom = new DOMDocument( '1.0' , 'utf8' ); $dom->recover = true; $dom->resolveExternals = false; libxml_use_internal_errors( true ); - $res = $dom->loadXML( $frag ); - + $xml = $dom->loadXML( $frag ); $err = libxml_get_errors(); libxml_clear_errors(); foreach( $err as $item ) { $msg = trim( $item->message ); + + if ( $item->code == 26 ) + continue; if ( str_starts_with( $msg , "Entity '" ) && str_ends_with( $msg , "' not defined" ) ) continue; - fwrite( STDERR , "\n XML load failed on entity file." ); - fwrite( STDERR , "\n Path: $path" ); - fwrite( STDERR , "\n Error: $msg\n" ); + print "\n XML load failed for entity file:"; + print "\n Path: $path"; + print "\n Error: $msg\n"; return; } - Entities::put( $path , $name , $text , replace: $expectedReplaced ); + Entities::put( $path , $name , $text ); } -function saveEntitiesFile( string $filename , array $entities ) +function outputFiles( string $filename , array $entities ) { $file = fopen( $filename , "w" ); - fputs( $file , "\n\n\n" ); + fputs( $file , "\n" ); + fputs( $file , "\n\n\n" ); $sepFileDir = __DIR__ . "/../temp/text-entities/"; From 88d13e7dda567a1292cb59b2154e606d7b5b8bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20L=20F=20S=20Bacci?= Date: Thu, 18 Jun 2026 15:05:23 -0300 Subject: [PATCH 11/11] Enable grouped entities inside doc-lang/reference --- configure.php | 8 +++---- scripts/text-entities.php | 48 ++++++++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/configure.php b/configure.php index 5ad9729337..5aafc87550 100755 --- a/configure.php +++ b/configure.php @@ -621,11 +621,9 @@ function dtd_text_entities() $php = $ac['PHP']; $lang = $ac["LANG"]; - $parts = - [ $php - , __DIR__ . "/scripts/text-entities.php" - , "en" - ]; + $parts = [ $php + , __DIR__ . "/scripts/text-entities.php" + , "en" ]; if ( $lang != "en" ) $parts[] = $lang; diff --git a/scripts/text-entities.php b/scripts/text-entities.php index d94a14cd53..43f7dfce4f 100644 --- a/scripts/text-entities.php +++ b/scripts/text-entities.php @@ -129,8 +129,11 @@ foreach( $langs as $lang ) { - loadDirEntities( $lang ); - loadDirReference( $lang ); + $entDir = __DIR__ . "/../../$lang/entities"; + $refDir = __DIR__ . "/../../$lang/reference"; + + loadDirEntities( $entDir ); + loadDirRecurse( $refDir ); Entities::$countLanguages++; } @@ -246,9 +249,8 @@ static function checkReplaces( bool $debug ) } } -function loadDirEntities( string $lang ) +function loadDirEntities( string $dir ) { - $dir = __DIR__ . "/../../$lang/entities"; $dir = realpath( $dir ); if ( $dir === false || ! is_dir( $dir ) ) { @@ -282,9 +284,22 @@ function loadDirEntities( string $lang ) } } -function loadDirReference( string $lang ) +function loadDirRecurse( string $dir ) { - // TODO + $paths = scandir( $dir ); + foreach( $paths as $path ) + { + if ( str_starts_with( $path , '.' ) ) + continue; + + $path = realpath( "$dir/$path" ); + + if ( is_dir( $path ) ) + loadDirRecurse( $path ); + else + if ( str_ends_with( $path , ".ent" ) ) + loadEntityGroup( $path ); + } } function loadEntityGroup( string $path ) @@ -310,7 +325,7 @@ function loadEntityGroup( string $path ) $remove = true; break; default: - print "\n Invalid translate value '$value' in '$path'.\n"; + print "\n Invalid translate attribute '$value' in '$path'.\n"; exit( 1 ); } @@ -319,22 +334,19 @@ function loadEntityGroup( string $path ) foreach( $list as $ent ) { + $name = $ent->getAttribute( "name" ); + // Weird, namespace correting, DOMNodeList -> DOMDocumentFragment transform - $other = new DOMDocument( '1.0' , 'utf8' ); + $other = new DOMDocument( '1.0' , 'utf8' ); foreach( $ent->childNodes as $node ) $other->appendChild( $other->importNode( $node , true ) ); - $name = $ent->getAttribute( "name" ); - $text = $other->saveXML(); - $text = str_replace( "&" , "&" , $text ); - - // Remove XML declaration and empty line added by libxml + // Piecewise reconstruct fragment, without XML declarations or extra newlines - $lines = explode( "\n" , $text ); - array_shift( $lines ); - array_pop( $lines ); - $text = implode( "\n" , $lines ); + $text = ""; + foreach( $other->childNodes as $node ) + $text .= $other->saveXML( $node ); Entities::put( $path , $name , $text , $unique , $remove ); } @@ -426,7 +438,7 @@ function outputFiles( string $filename , array $entities ) // Slow path: entity body as an external file, // as to avoid (re)quotation hell. - $path = $sepFileDir . "/{$entity->name}.ent"; + $path = $sepFileDir . "/{$entity->name}.xml"; if ( file_exists( $path ) ) {