Your IP : 216.73.216.240


Current Path : /home/juvelize/saulnois/libraries/ic_library/Vendor/Icalcreator/src/Xml/
Upload File :
Current File : /home/juvelize/saulnois/libraries/ic_library/Vendor/Icalcreator/src/Xml/Parser.php

<?php
/**
 * iCalcreator, the PHP class package managing iCal (rfc2445/rfc5445) calendar information.
 *
 * This file is a part of iCalcreator.
 *
 * @author    Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
 * @copyright 2007-2024 Kjell-Inge Gustafsson, kigkonsult AB, All rights reserved
 * @link      https://kigkonsult.se
 * @license   Subject matter of licence is the software iCalcreator.
 *            The above copyright, link, package and version notices,
 *            this licence notice and the invariant [rfc5545] PRODID result use
 *            as implemented and invoked in iCalcreator shall be included in
 *            all copies or substantial portions of the iCalcreator.
 *
 *            iCalcreator is free software: you can redistribute it and/or modify
 *            it under the terms of the GNU Lesser General Public License as
 *            published by the Free Software Foundation, either version 3 of
 *            the License, or (at your option) any later version.
 *
 *            iCalcreator is distributed in the hope that it will be useful,
 *            but WITHOUT ANY WARRANTY; without even the implied warranty of
 *            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *            GNU Lesser General Public License for more details.
 *
 *            You should have received a copy of the GNU Lesser General Public License
 *            along with iCalcreator. If not, see <https://www.gnu.org/licenses/>.
 */
declare( strict_types = 1 );
namespace Kigkonsult\Icalcreator\Xml;

use Exception;
use Kigkonsult\Icalcreator\CalendarComponent;
use Kigkonsult\Icalcreator\IcalInterface;
use Kigkonsult\Icalcreator\Util\StringFactory;
use Kigkonsult\Icalcreator\Vcalendar;

use function html_entity_decode;
use function in_array;
use function is_array;
use function sprintf;
use function str_replace;
use function str_starts_with;
use function stripos;
use function strlen;
use function strtolower;
use function strtoupper;
use function substr;
use function trim;
use function ucfirst;

/**
 * iCalcreator XML (rfc6321) parser class
 *
 * @since 2.41.88 2024-01-16
 */
final class Parser extends XmlBase
{
    /**
     * Parse (rfc6321) XML string into iCalcreator instance
     *
     * @param string $xmlStr
     * @param null|string[] $iCalcfg Vcalendar config array (opt)
     * @return Vcalendar|bool   false on error
     * @throws Exception
     * @since 2.41.69 2022-10-04
     */
    public static function XML2iCal( string $xmlStr, ? array $iCalcfg = [] ) : Vcalendar | bool
    {
        static $CRLF = [ "\r\n", "\n\r", "\n", "\r" ];
        $xmlStr      = str_replace( $CRLF, StringFactory::$SP0, $xmlStr );
        $xml         = self::XMLgetTagContent1( $xmlStr, self::$Vcalendar, $endIx );
        $iCal        = new Vcalendar( $iCalcfg ?? [] );
        if( false === self::XMLgetComps( $iCal, $xmlStr )) {
            return false;
        }
        return $iCal;
    }

    /**
     * Parse (rfc6321) XML string into iCalcreator components
     *
     * @ param IcalInterface $iCal
     * @param Vcalendar|CalendarComponent $iCal
     * @param string    $xml
     * @return IcalInterface|bool    false on error
     * @since 2.41.69 2022-10-04
     */
    private static function XMLgetComps( Vcalendar|CalendarComponent $iCal, string $xml ) : IcalInterface | bool
    {
        static $PROPSTAGempty = '<properties/>';
        static $PROPSTAGstart = '<properties>';
        static $COMPSTAGempty = '<components/>';
        static $COMPSTAGstart = '<components>';
        static $NEW      = 'new';
        static $ALLCOMPS = [ // all IcalBase::$CALCOMPS + IcalBase::$TZCOMPS
            IcalInterface::AVAILABLE,
            IcalInterface::DAYLIGHT,
            IcalInterface::PARTICIPANT,
            IcalInterface::STANDARD,
            IcalInterface::VALARM,
            IcalInterface::VAVAILABILITY,
            IcalInterface::VEVENT,
            IcalInterface::VFREEBUSY,
            IcalInterface::VJOURNAL,
            IcalInterface::VLOCATION,
            IcalInterface::VRESOURCE,
            IcalInterface::VTIMEZONE,
            IcalInterface::VTODO,
        ];
        $len = strlen( $xml );
        $sx  = 0;
        while(
            ((( $sx + 12 ) < $len ) &&
                ! str_starts_with( substr( $xml, $sx ), $PROPSTAGstart ) &&
                ! str_starts_with( substr( $xml, $sx ), $COMPSTAGstart )
            ) &&
            ((( $sx + 13 ) < $len ) &&
                ! str_starts_with( substr( $xml, $sx ), $PROPSTAGempty ) &&
                ! str_starts_with( substr( $xml, $sx ), $COMPSTAGempty ))) {
            ++$sx;
        } // end while
        if(( $sx + 11 ) >= $len ) {
            return false;
        }
        if( str_starts_with( $xml, $PROPSTAGempty )) {
            $pos = strlen( $PROPSTAGempty );
            $xml = substr( $xml, $pos );
        }
        elseif( str_starts_with( substr( $xml, $sx ), $PROPSTAGstart )) {
            $xml2 = self::XMLgetTagContent1( $xml, self::$properties, $endIx );
            self::XMLgetProps( $iCal, $xml2 );
            $xml  = substr( $xml, $endIx );
        }
        if( str_starts_with( $xml, $COMPSTAGempty )) {
            $pos = strlen( $COMPSTAGempty );
            $xml = substr( $xml, $pos );
        }
        elseif( str_starts_with( $xml, $COMPSTAGstart )) {
            $xml = self::XMLgetTagContent1( $xml, self::$components, $endIx );
        }
        while( ! empty( $xml )) {
            $xml2     = self::XMLgetTagContent2( $xml, $tagName, $endIx );
            $compType = ucfirst( strtolower( $tagName ));
            if( in_array( $compType, $ALLCOMPS, true )) {
                $iCalComp      = $iCal->{$NEW . $compType}();
                self::XMLgetComps( $iCalComp, $xml2 );
            }
            $xml = substr( $xml, $endIx );
        } // end while( ! empty( $xml ))
        return $iCal;
    }

    /**
     * Parse (rfc6321) XML into iCalcreator properties
     *
     * @param Vcalendar|CalendarComponent $iCalComp
     * @param string $xml
     * @return void
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetProps( Vcalendar|CalendarComponent $iCalComp, string $xml ) : void
    {
        static $PARAMENDTAG = '<parameters/>';
        static $PARAMTAG    = '<parameters>';
        $endIx3       = strlen( $xml ) + 1; // i.e. init var as int
        while( ! empty( $xml )) {
            $xml2     = self::XMLgetTagContent2( $xml, $propName, $endIx );
            $propName = strtoupper( $propName );
            if( empty( $xml2 ) && ( StringFactory::$ZERO !== $xml2 )) {
                self::XMLgetEmptyProp( $iCalComp, $propName );
                $xml = substr( $xml, $endIx );
                continue;
            } // end if
            $params = [];
            if( str_starts_with( $xml2, $PARAMENDTAG )) { // no params
                $xml2 = substr( $xml2, 13 );
            }
            elseif( str_starts_with( $xml2, $PARAMTAG )) {
                $xml3   = self::XMLgetTagContent1( $xml2, self::$PARAMETERS, $endIx2 );
                self::XMLgetPropParams( $xml3, $params, $endIx3 );
                $xml2 = substr( $xml2, $endIx2 );
            } // end elseif - parameters
            $valueType = StringFactory::$SP0;
            $value     = ( ! empty( $xml2 ) || ( StringFactory::$ZERO === $xml2 ))
                ? self::XMLgetTagContent2( $xml2, $valueType, $endIx3 )
                : StringFactory::$SP0;
            match( $propName ) { // get/prep property value
                IcalInterface::URL, IcalInterface::TZURL =>
                    $value = html_entity_decode( $value ),
                IcalInterface::EXDATE, IcalInterface::RDATE, IcalInterface::FREEBUSY =>
                    $value = self::XMLgetRexFreeBProp( $propName, $xml2, $valueType, $params ),
                IcalInterface::TZOFFSETTO, IcalInterface::TZOFFSETFROM =>
                    $value = str_replace( StringFactory::$COLON, StringFactory::$SP0, $value ),
                IcalInterface::GEO =>
                    $value = self::XMLgetGeoProp( $xml2, $value ),
                IcalInterface::EXRULE, IcalInterface::RRULE =>
                    $value = self::XMLgetRexRuleProp( substr( $xml2, $endIx3 ), [ $valueType => $value ] ),
                IcalInterface::REQUEST_STATUS =>
                    $value = self::XMLgetReqStatProp( $xml2 ),
                IcalInterface::STRUCTURED_DATA =>
                    self::XMLStrucDataProp( $valueType, $params ),
                IcalInterface::STYLED_DESCRIPTION =>
                    self::XMLStyldDescrProp( $valueType, $params ),
                default =>
                    $value = self::XMLgetDefaultProp( $valueType, $value, $propName, $params )
            }; // end match( $propName )
            self::XMLsetIcalProp( $iCalComp, $propName, $value, $params );
            $xml = substr( $xml, $endIx );
        } // end while( ! empty( $xml ))
    }

    /**
     * Manage an empty property
     *
     * @param Vcalendar|CalendarComponent $iCalComp
     * @param string                      $propName
     * @return void
     */
    private static function XMLgetEmptyProp( Vcalendar|CalendarComponent $iCalComp, string $propName ) : void
    {
        if( StringFactory::isXprefixed( $propName )) {
            $iCalComp->setXprop( $propName );
            return;
        }
        $method = StringFactory::getSetMethodName( $propName );
        $iCalComp->{$method}();
    }


    /**
     * Manage prop params
     *
     * @param string $xml3
     * @param string[]|string[][] $params
     * @param int|null $endIx
     * @return void
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetPropParams( string $xml3, array & $params, ? int & $endIx = null ) : void
    {
        static $ATTENDEEPARKEYS = [
            IcalInterface::DELEGATED_FROM,
            IcalInterface::DELEGATED_TO,
            IcalInterface::MEMBER
        ];
        $endIx3 = 0;
        while( ! empty( $xml3 )) {
            $xml4     = self::XMLgetTagContent2( $xml3, $paramKey, $endIx3 );
            $paramKey = strtoupper( $paramKey );
            if( in_array( $paramKey, $ATTENDEEPARKEYS, true )) {
                while( ! empty( $xml4 )) {
                    $paramValue = self::XMLgetTagContent1( $xml4, self::$cal_address, $endIx4 );
                    if( ! isset( $params[$paramKey] )) {
                        $params[$paramKey] = [ $paramValue ];
                    }
                    else {
                        $params[$paramKey][] = $paramValue;
                    }
                    $xml4 = substr( $xml4, $endIx4 );
                } // end while
            } // end if( in_array( $paramKey, $ATTENDEEPARKEYS ))
            else {
                $pType      = StringFactory::$SP0; // skip parameter valueType
                $paramValue = html_entity_decode(
                    self::XMLgetTagContent2( $xml4, $pType, $endIx4 )
                );
                if( ! isset( $params[$paramKey] )) {
                    $params[$paramKey] = $paramValue;
                }
                else {
                    $params[$paramKey] .= StringFactory::$COMMA . $paramValue;
                }
            }
            $xml3 = substr( $xml3, $endIx3 );
        } // end while
    }

    /**
     * Manage Rdate prop
     *
     * @param string $propName
     * @param string $xml2
     * @param string $valueType
     * @param string[] $params
     * @return string[]
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetRexFreeBProp(
        string $propName,
        string $xml2,
        string $valueType,
        array & $params
    ) : array
    {
        static $REXDATE = [ IcalInterface::EXDATE, IcalInterface::RDATE ];
        if( in_array( $propName, $REXDATE ) &&
            ( self::$period !== $valueType )) {
            return self::XMLgetRdateProp( $xml2, $valueType, $params );
        }
        if( IcalInterface::FREEBUSY !== $propName ) {
            $params[ IcalInterface::VALUE ] = IcalInterface::PERIOD;
        }
        return self::XMLgetPeriodProp( $xml2 );
    }

    /**
     * Manage Rdate prop
     *
     * @param string $xml2
     * @param string $valueType
     * @param string[] $params
     * @return string[]
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetRdateProp( string $xml2, string $valueType, array & $params ) : array
    {
        static $DATETAGST = '<date';
        if( self::$date === $valueType ) {
            $params[IcalInterface::VALUE] = IcalInterface::DATE;
        }
        $t = [];
        while( ! empty( $xml2 ) && str_starts_with( $xml2, $DATETAGST )) {
            $t[]  = self::XMLgetTagContent2( $xml2, $pType, $endIx4);
            $xml2 = substr( $xml2, $endIx4 );
        } // end while
        return $t;
    }

    /**
     * Manage a period typed prop
     *
     * @param string $xml2
     * @return string[]
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetPeriodProp( string $xml2 ) : array
    {
        static $PERIODTAG = '<period>';
        $value            = [];
        while( ! empty( $xml2 ) && str_starts_with( $xml2, $PERIODTAG )) {
            $xml3 = self::XMLgetTagContent1( $xml2, self::$period, $endIx4);
            $t    = [];
            while( ! empty( $xml3 )) { // start - end/duration
                $t[]  = self::XMLgetTagContent2( $xml3, $pType, $endIx5 );
                $xml3 = substr( $xml3, $endIx5 );
            } // end while
            $value[] = $t;
            $xml2    = substr( $xml2, $endIx4 );
        } // end while
        return $value;
    }

    /**
     * Manage Geo prop
     *
     * @param string   $xml2
     * @param string   $value
     * @param null|int $endIx3
     * @return string[]
     * @since 2.41.88 2024-01-20
     */
    private static function XMLgetGeoProp( string $xml2, string $value, ? int & $endIx3 = 0  ) : array
    {
        return [
            IcalInterface::LATITUDE  => $value,
            IcalInterface::LONGITUDE => self::XMLgetTagContent1(
                substr( $xml2, $endIx3 ),
                IcalInterface::LONGITUDE,
                $endIx3
            )
        ];
    }

    /**
     * Manage ExRule/RexRule prop
     *
     * @param string $xml2
     * @param string[] $tValue
     *     $valueType => $value
     * @return string[]
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetRexRuleProp( string $xml2, array $tValue ) : array
    {
        $valueType = StringFactory::$SP0;
        while( ! empty( $xml2 )) {
            $t     = self::XMLgetTagContent2( $xml2, $valueType, $endIx4 );
            $valueType = strtoupper( $valueType );
            switch( $valueType ) {
                case IcalInterface::FREQ :     // fall through
                case IcalInterface::COUNT :    // fall through
                case IcalInterface::INTERVAL : // fall through
                case IcalInterface::RSCALE :   // fall through
                case IcalInterface::SKIP :     // fall through
                case IcalInterface::UNTIL :    // fall through
                case IcalInterface::WKST :
                    $tValue[$valueType] = $t;
                    break;
                case IcalInterface::BYDAY :
                    self::assureElementIsArray( $tValue, IcalInterface::BYDAY );
                    $tLen = strlen( $t );
                    if( 2 === $tLen ) {
                        self::addElementValue(
                            $tValue,
                            IcalInterface::BYDAY,
                            [ IcalInterface::DAY => $t ]
                        );
                    }
                    else {
                        $day = substr( $t, -2 );
                        $key = substr( $t, 0, ( $tLen - 2 ));
                        self::addElementValue(
                            $tValue,
                            IcalInterface::BYDAY,
                            [ $key, IcalInterface::DAY => $day ]
                        );
                    }
                    break;
                default:
                    self::assureElementIsArray( $tValue, $valueType );
                    self::addElementValue( $tValue, $valueType, $t );
                    break;
            } // end switch
            $xml2 = substr( $xml2, $endIx4 );
        } // end while
        return $tValue;
    }

    /**
     * Manage Req-Stat p
     *
     * @param string $xml2
     * @return string[]
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetReqStatProp( string $xml2 ) : array
    {
        $value = [
            self::$code        => null,
            self::$description => null,
            self::$data        => null
        ];
        while( ! empty( $xml2 )) {
            $t    = html_entity_decode(
                self::XMLgetTagContent2( $xml2, $valueType, $endIx4 )
            );
            $value[$valueType] = $t;
            $xml2 = substr( $xml2, $endIx4 );
        } // end while
        return $value;
    }

    /**
     * Manage STRUCTURED_DATA prop
     *
     * @param string $valueType
     * @param mixed[] $params
     * @return void
     * @since 2.41.88 2024-01-16
     */
    private static function XMLStrucDataProp( string $valueType, array & $params ) : void
    {
        $params[IcalInterface::VALUE] = match( $valueType ) {
            self::$binary => IcalInterface::BINARY,
            self::$text   => IcalInterface::TEXT,
            self::$uri    => IcalInterface::URI,
        };
    }

    /**
     * Manage STYLED_DESCRIPTION prop
     *
     * @param string $valueType
     * @param mixed[] $params
     * @return void
     * @since 2.41.88 2024-01-16
     */
    private static function XMLStyldDescrProp( string $valueType, array & $params ) : void
    {
        $params[IcalInterface::VALUE] = match( $valueType ) {
            self::$text => IcalInterface::TEXT,
            self::$uri  => IcalInterface::URI
        };
    }

    /**
     * Manage whatever left prop
     *
     * @param string $valueType
     * @param string $value
     * @param string $propName
     * @param string[] $params
     * @return string
     * @since 2.41.88 2024-01-16
     */
    private static function XMLgetDefaultProp(
        string $valueType,
        string $value,
        string $propName,
        array & $params
    ) : string
    {
        static $ATTSRC = [ IcalInterface::ATTACH, IcalInterface::SOURCE ];
        switch( $valueType ) {
            case self::$uri :
                $value = html_entity_decode( $value );
                if( in_array( $propName, $ATTSRC, true )) {
                    break;
                }
                $params[IcalInterface::VALUE] = IcalInterface::URI;
                break;
            case self::$binary :
                $params[IcalInterface::VALUE] = IcalInterface::BINARY;
                break;
            case self::$date :
                $params[IcalInterface::VALUE] = IcalInterface::DATE;
                break;
            case self::$date_time :
                $params[IcalInterface::VALUE] = IcalInterface::DATE_TIME;
                break;
            case self::$text :
                // fall through
            case self::$unknown :
                $value = html_entity_decode( $value );
                break;
            default :
                if( StringFactory::isXprefixed( $propName ) &&
                    ( self::$unknown !== strtolower( $valueType ))) {
                    $params[IcalInterface::VALUE] = strtoupper( $valueType );
                }
                break;
        } // end switch
        return $value;
    }

    /**
     * @param CalendarComponent|Vcalendar $iCalComp
     * @param string                      $propName
     * @param null|string|array           $value
     * @param array                       $params
     * @return void
     */
    private static function XMLsetIcalProp(
        CalendarComponent|Vcalendar $iCalComp,
        string $propName,
        null|string|array $value,
        array $params
    ) : void
    {
        static $VERSIONPRODID = [ IcalInterface::VERSION, IcalInterface::PRODID ];
        static $REXRULE       = [ IcalInterface::EXRULE, IcalInterface::RRULE ];
        $method = StringFactory::getSetMethodName( $propName );
        switch( true ) {
            case ( in_array( $propName, $VERSIONPRODID, true )) :
                break;
            case ( StringFactory::isXprefixed( $propName )) :
                $iCalComp->setXprop( $propName, $value, $params );
                break;
            case ( in_array( $propName, $REXRULE, true ) &&
                isset( $value[self::$recur] ) && empty( $value[self::$recur] )) :
                $iCalComp->{$method}(); // empty rexRule
                break;
            case ( IcalInterface::FREEBUSY === $propName ) :
                $fbtype = $params[IcalInterface::FBTYPE] ?? null;
                unset( $params[IcalInterface::FBTYPE] );
                $iCalComp->{$method}( $fbtype, $value, $params );
                break;
            case ( IcalInterface::GEO === $propName ) :
                $input = $iCalComp::extractGeoLatLong( $value );
                $iCalComp->{$method}( $input[0], $input[1], ( empty( $input[0] ) ? [] : $params ));
                break;
            case ( IcalInterface::REQUEST_STATUS === $propName ) :
                $input = $iCalComp::extractRequeststatus( $value );
                $iCalComp->{$method}( $input[0], $input[1], $input[2], ( empty( $input[1] ) ? [] : $params ));
                break;
            default :
                if( empty( $value ) && ( is_array( $value ) || ( StringFactory::$ZERO > $value ))) {
                    $value = null;
                }
                $iCalComp->{$method}( $value, $params );
                break;
        } // end switch
    }

    /**
     * @param array $array
     * @param string $key
     */
    private static function assureElementIsArray( array & $array, string $key ) : void
    {
        if( ! isset( $array[$key] )) {
            $array[$key] = [];
        }
    }

    /**
     * @param array $array
     * @param string $key
     * @param int|string|array $value
     */
    private static function addElementValue( array & $array, string $key, int|string|array $value ) : void
    {
        $array[$key][] = $value;
    }

    /**
     * Fetch a specific XML tag content
     *
     * @param string   $xml
     * @param string   $tagName
     * @param null|int $endIx
     * @return string
     * @since 2.41.69 2022-10-04
     */
    private static function XMLgetTagContent1( string $xml, string $tagName, ? int & $endIx = 0 ) : string
    {
        static $FMT0 = '<%s>';
        static $FMT1 = '<%s />';
        static $FMT2 = '<%s/>';
        static $FMT3 = '</%s>';
        $tagName = strtolower( $tagName );
        $strLen  = strlen( $tagName );
        $xmlLen  = strlen( $xml );
        $sx1     = 0;
        while( $sx1 < $xmlLen ) {
            if((( $sx1 + $strLen + 1 ) < $xmlLen ) && // start tag
                ( sprintf( $FMT0, $tagName ) === strtolower( substr( $xml, $sx1, ( $strLen + 2 ))))
            ) {
                break;
            }
            if((( $sx1 + $strLen + 3 ) < $xmlLen ) && // empty tag1
                ( sprintf( $FMT1, $tagName ) === strtolower( substr( $xml, $sx1, ( $strLen + 4 ))))
            ) {
                $endIx = $strLen + 5;
                return StringFactory::$SP0; // empty tag
            }
            if((( $sx1 + $strLen + 2 ) < $xmlLen ) && // empty tag2
                ( sprintf( $FMT2, $tagName ) ===  strtolower( substr( $xml, $sx1, ( $strLen + 3 ))))
            ) {
                $endIx = $strLen + 4;
                return StringFactory::$SP0; // empty tag
            }
            ++$sx1;
        } // end while...
        if( ! isset( $xml[$sx1] )) {
            $endIx = ( empty( $sx1 )) ? 0 : $sx1 - 1; // ??
            return StringFactory::$SP0;
        }
        $endTag = sprintf( $FMT3, $tagName );
        if( false === ( $pos = stripos( $xml, $endTag ))) { // missing end tag??
            $endIx = $xmlLen + 1;
            return StringFactory::$SP0;
        }
        $endIx = $pos + $strLen + 3;
        $start = $sx1 + $strLen + 2;
        $len   = $pos - $sx1 - 2 - $strLen;
        return substr( $xml, $start, $len );
    }

    /**
     * Fetch next (unknown) XML tagname AND content
     *
     * @param string $xml
     * @param string|null $tagName
     * @param int|null $endIx
     * @return string
     * @since 2.41.69 2022-10-04
     */
    private static function XMLgetTagContent2( string $xml, ? string & $tagName = null, ? int & $endIx = null ) : string
    {
        static $LT          = '<';
        static $CMTSTART    = '<!--';
        static $EMPTYTAGEND = '/>';
        static $GT          = '>';
        static $DURATION    = 'duration';
        static $DURATIONTAG = '<duration>';
        static $DURENDTAG   = '</duration>';
        static $FMTTAG      = '</%s>';
        $xmlLen = strlen( $xml );
        $endIx  = $xmlLen + 1; // just in case.. .
        $sx1    = 0;
        while( $sx1 < $xmlLen ) {
            if( $LT === $xml[$sx1] ) {
                if((( $sx1 + 3 ) < $xmlLen ) &&
                    str_starts_with( substr( $xml, $sx1 ), $CMTSTART )) { // skip comment
                    ++$sx1;
                }
                else {
                    break;
                } // tagname start here
            }
            else {
                ++$sx1;
            }
        } // end while...
        $sx2 = $sx1;
        while( $sx2 < $xmlLen ) {
            if((( $sx2 + 1 ) < $xmlLen ) &&
                str_starts_with( substr( $xml, $sx2 ), $EMPTYTAGEND )) { // tag with no content
                $tagName = trim( substr( $xml, ( $sx1 + 1 ), ( $sx2 - $sx1 - 1 )));
                $endIx   = $sx2 + 2;
                return StringFactory::$SP0;
            }
            if( $GT === $xml[$sx2] ) { // tagname ends here
                break;
            }
            ++$sx2;
        } // end while...
        $tagName = substr( $xml, ( $sx1 + 1 ), ( $sx2 - $sx1 - 1 ));
        $endIx   = $sx2 + 1;
        if( $sx2 >= $xmlLen ) {
            return StringFactory::$SP0;
        }
        $strLen = strlen( $tagName );
        if(( $DURATION === $tagName ) &&
            ( false !== ( $pos1 = stripos( $xml, $DURATIONTAG, $sx1 + 1 ))) &&
            ( false !== ( $pos2 = stripos( $xml, $DURENDTAG,  $pos1 + 1 ))) &&
            ( false !== ( $pos3 = stripos( $xml, $DURENDTAG,  $pos2 + 1 ))) &&
            ( $pos1 < $pos2 ) && ( $pos2 < $pos3 )) {
            $pos = $pos3;
        }
        elseif( false === ( $pos = stripos( $xml, sprintf( $FMTTAG, $tagName ), $sx2 ))) {
            return StringFactory::$SP0;
        }
        $endIx = $pos + $strLen + 3;
        return substr( $xml, ( $sx1 + $strLen + 2 ), ( $pos - $strLen - 2 ));
    }
}