Convert a document to a new format and return a conversion object relating to the conversion in progress.

@param   \core_files\conversion $conversion The file to be converted
@return  this
163890
public function start_document_conversion(\core_files\conversion $conversion) {
        global $CFG;

        $file = $conversion->get_sourcefile();
        $format = $conversion->get('targetformat');

        $issuerid = get_config('fileconverter_googledrive', 'issuerid');
        if (empty($issuerid)) {
            $conversion->set('status', conversion::STATUS_FAILED);
            return $this;
        }

        $issuer = \core\oauth2\api::get_issuer($issuerid);
        if (empty($issuer)) {
            $conversion->set('status', conversion::STATUS_FAILED);
            return $this;
        }
        $client = \core\oauth2\api::get_system_oauth_client($issuer);

        $service = new \fileconverter_googledrive\rest($client);

        $contenthash = $file->get_contenthash();

        $originalname = $file->get_filename();
        if (strpos($originalname, '.') === false) {
            $conversion->set('status', conversion::STATUS_FAILED);
            return $this;
        }
        $importextension = substr($originalname, strrpos($originalname, '.') + 1);

        $importformat = self::$imports[$importextension];
        $exportformat = self::$exports[$format];

        $metadata = [
            'name' => $contenthash,
            'mimeType' => $importformat
        ];

        $filecontent = $file->get_content();
        $filesize = $file->get_filesize();
        $filemimetype = $file->get_mimetype();

        // Start resumable upload.
        // First create empty file.
        $params = [
            'uploadType' => 'resumable',
            'fields' => 'id,name'
        ];

        $client->setHeader('X-Upload-Content-Type: ' . $filemimetype);
        $client->setHeader('X-Upload-Content-Length: ' . $filesize);

        $headers = $service->call('upload', $params, json_encode($metadata));

        $uploadurl;
        // Google returns a location header with the location for the upload.
        foreach ($headers as $header) {
            if (strpos($header, 'Location:') === 0) {
                $uploadurl = trim(substr($header, strpos($header, ':') + 1));
            }
        }

        if (empty($uploadurl)) {
            $conversion->set('status', conversion::STATUS_FAILED);
            return $this;
        }

        $params = [
            'uploadurl' => $uploadurl
        ];
        $result = $service->call('upload_content', $params, $filecontent, $filemimetype);

        $fileid = $result->id;
        // Now export it again.
        $params = ['mimeType' => $exportformat];
        $sourceurl = new moodle_url('https://www.googleapis.com/drive/v3/files/' . $fileid . '/export', $params);
        $source = $sourceurl->out(false);

        $tmp = make_request_directory();
        $downloadto = $tmp . '/' . $fileid . '.' . $format;

        $options = ['filepath' => $downloadto, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
        $success = $client->download_one($source, null, $options);

        if ($success) {
            $conversion->store_destfile_from_path($downloadto);
            $conversion->set('status', conversion::STATUS_COMPLETE);
            $conversion->update();
        } else {
            $conversion->set('status', conversion::STATUS_FAILED);
        }
        // Cleanup.
        $params = [
            'fileid' => $fileid
        ];
        $service->call('delete', $params);

        return $this;
    }
954801
protected function convertResource($convertTo)
    {
        $this->tmpImage[] = $tmpImage = tempnam(sys_get_temp_dir(), 'picture');

        if($this->isGd())
        {
            imagepng($this->resource, $tmpImage, 0);
        }
        else
        {
            $this->resource->writeImage($tmpImage);
        }

        if($convertTo === $this::WORKER_GD)
        {
            return $this->getGdResource($tmpImage);
        }
        else
        {
            return $this->createImagick($tmpImage);
        }
    }
707234
public function convert(DOMDocument $document)
    {
        $xpath = new DOMXPath($document);
        $xpathExpression = '//ns:pre';
        $ns = $document->documentElement->namespaceURI;
        $xpath->registerNamespace('ns', $ns);
        $elements = $xpath->query($xpathExpression);

        foreach ($elements as $element) {
            $element->textContent = str_replace(']]>', ']]]]><![CDATA[>', $element->textContent);
        }

        return $document;
    }
489747
public function doConvert()
    {
        $beginTime = microtime(true);

        $this->activateWarningLogger();
        //set_error_handler(array($this, "errorHandler"));

        try {
            // Prepare options
            //$this->prepareOptions();

            $this->checkOptions();

            // Prepare destination folder
            $this->createWritableDestinationFolder();
            $this->removeExistingDestinationIfExists();

            if (!isset($this->options['_skip_input_check'])) {
                // Run basic input validations (if source exists and if file extension is valid)
                $this->checkInput();

                // Check that a file can be written to destination
                $this->checkFileSystem();
            }

            $this->checkOperationality();
            $this->checkConvertability();
            $this->runActualConvert();
        } catch (ConversionFailedException $e) {
            $this->deactivateWarningLogger();
            throw $e;
        } catch (\Exception $e) {
            $this->deactivateWarningLogger();
            throw new UnhandledException('Conversion failed due to uncaught exception', 0, $e);
        } catch (\Error $e) {
            $this->deactivateWarningLogger();
            // https://stackoverflow.com/questions/7116995/is-it-possible-in-php-to-prevent-fatal-error-call-to-undefined-function
            //throw new UnhandledException('Conversion failed due to uncaught error', 0, $e);
            throw $e;
        }
        $this->deactivateWarningLogger();

        $source = $this->source;
        $destination = $this->destination;

        if (!@file_exists($destination)) {
            throw new ConversionFailedException('Destination file is not there: ' . $destination);
        } elseif (@filesize($destination) === 0) {
            unlink($destination);
            throw new ConversionFailedException('Destination file was completely empty');
        } else {
            if (!isset($this->options['_suppress_success_message'])) {
                $this->ln();
                $msg = 'Converted image in ' .
                    round((microtime(true) - $beginTime) * 1000) . ' ms';

                $sourceSize = @filesize($source);
                if ($sourceSize !== false) {
                    $msg .= ', reducing file size with ' .
                        round((filesize($source) - filesize($destination))/filesize($source) * 100) . '% ';

                    if ($sourceSize < 10000) {
                        $msg .= '(went from ' . round(filesize($source)) . ' bytes to ';
                        $msg .= round(filesize($destination)) . ' bytes)';
                    } else {
                        $msg .= '(went from ' . round(filesize($source)/1024) . ' kb to ';
                        $msg .= round(filesize($destination)/1024) . ' kb)';
                    }
                }
                $this->logLn($msg);
            }
        }
    }
550350
public function beforeConvert(ConvertEvent $convertEvent): void
    {
        if ($convertEvent->isHandledBy(SculpinMarkdownBundle::CONVERTER_NAME, SculpinTwigBundle::FORMATTER_NAME)) {
            $content = $convertEvent->source()->content();
            foreach (self::$addPlaceholderRe as $re) {
                $content = preg_replace($re, self::$placeholder, $content);
            }
            $content = preg_replace(
                '/{%\s+(\w+)\s+(\w+\++\w+)\s+%}(.*){%\s+end\1\s+%}/Us',
                '<div data-$1="$2">$3</div>',
                $content
            );
            $convertEvent->source()->setContent($content);
        }
    }
1369500
public function ConvertObjectForm() {
        //Verify the record exists before proceeding
        if(empty($this->record) || $this->record===false || !$this->record->exists()) {
            return $this->httpError(404, _t('KapostAdmin.KAPOST_CONTENT_NOT_FOUND', '_Incoming Kapost Content could not be found'));
        }
        
        //Reset the reading stage
        Versioned::reset();
        
        
        $fields=new FieldList(
                            $wrapper=CompositeField::create(
                                    CompositeField::create(
                                            $convertModeField=new OptionsetField('ConvertMode', '', array(
                                                                                                        'ReplacePage'=>_t('KapostAdmin.REPLACES_AN_EXISTING_PAGE', '_This replaces an existing page'),
                                                                                                        'NewPage'=>_t('KapostAdmin.IS_NEW_PAGE', '_This is a new page')
                                                                                                    ), 'NewPage')
                                        )->addExtraClass('kapostConvertLeftSide noborder')->setName('kapostConvertLeftSide'),
                                    CompositeField::create(
                                            $replacePageField=TreeDropdownField::create('ReplacePageID', _t('KapostAdmin.REPLACE_PAGE', '_Replace this page'), 'SiteTree')->addExtraClass('replace-page-id'),
                                            TreeDropdownField::create('ParentPageID', _t('KapostAdmin.USE_AS_PARENT', '_Use this page as the parent for the new page, leave empty for a top level page'), 'SiteTree')->addExtraClass('parent-page-id')
                                        )->addExtraClass('kapostConvertRightSide')->setName('kapostConvertRightSide')
                                )->addExtraClass('kapostConvertTypeWrap')->setName('kapostConvertTypeWrap')
                        );
        
        
        $actions=new FieldList(
                                FormAction::create('doConvertObject', _t('KapostAdmin.CONTINUE_CONVERT', '_Continue'))
                                    ->setUseButtonTag(true)
                                    ->addExtraClass('ss-ui-action-constructive')
                                    ->setAttribute('data-icon', 'kapost-convert')
                            );
        
        
        $validator=new RequiredFields(
                                        'ConvertMode'
                                    );
        
        
        $form=new Form($this, 'ConvertObjectForm', $fields, $actions, $validator);
        $form
            ->addExtraClass('KapostAdmin kapost-conversion-form')
            ->setTemplate('KapostAdmin_ConvertForm');
        
        
        //Handle pages to see if the page exists
        $convertToClass=$this->getDestinationClass();
        if($convertToClass!==false && ($convertToClass=='SiteTree' || is_subclass_of($convertToClass, 'SiteTree'))) {
            $obj=SiteTree::get()->filter('KapostRefID', Convert::raw2sql($this->record->KapostRefID))->first();
            if(!empty($obj) && $obj!==false && $obj->ID>0) {
                $form->Fields()->insertBefore('kapostConvertTypeWrap', HiddenField::create('ConvertMode', 'ConvertMode', 'ReplacePage')->setForm($form));
                $form->Fields()->insertBefore('kapostConvertTypeWrap', HiddenField::create('ReplacePageID', 'ReplacePageID', $obj->ID)->setForm($form));
                $form->Fields()->removeByName('kapostConvertTypeWrap');
                
                $recordTitle=$this->record->Title;
                if(!empty($recordTitle) && $recordTitle!=$obj->Title) {
                    $urlFieldLabel=_t('KapostAdmin.TITLE_CHANGE_DETECT', '_The title differs from the page being replaced, it was "{wastitle}" and will be changed to "{newtitle}". Do you want to update the URL Segment?', array(
                                            'wastitle'=>$obj->Title,
                                            'newtitle'=>$recordTitle
                                        ));
                    
                    $fields->insertAfter(CheckboxField::create('UpdateURLSegment', $urlFieldLabel)
                                                            ->addExtraClass('urlsegmentcheck')
                                                            ->setAttribute('data-replace-id', $obj->ID)
                                                            ->setForm($form)
                                                            ->setDescription(_t('KapostAdmin.NEW_URL_SEGMENT', '_The new URL Segment will be or will be close to "{newsegment}"', array(
                                                                                                                                            'newsegment'=>$obj->generateURLSegment($recordTitle)
                                                                                                                                        ))), 'ReplacePageID');
                }
            }
        }
        
        
        Requirements::css(KAPOST_DIR.'/css/KapostAdmin.css');
        
        Requirements::add_i18n_javascript(KAPOST_DIR.'/javascript/lang/');
        Requirements::javascript(KAPOST_DIR.'/javascript/KapostAdmin_convertPopup.js');
        
        
        //Allow extensions to adjust the form
        $this->extend('updateConvertObjectForm', $form, $this->record);
        
        
        //If we have more than 1 field enable the line below the convert type
        $csrfToken=$form->getSecurityToken();
        if((!($csrfToken instanceof NullSecurityToken) && $form->Fields()->count()>2) || ($csrfToken instanceof NullSecurityToken && $form->Fields()->count()>1)) {
            $wrapper->addExtraClass('has-more-fields');
        }
        
        
        return $form;
    }
1351057
public function convertToJson($convert = true) {

		if(false === is_bool($convert)) {
			return trigger_error(sprintf('Argument 1 passed to %s() must be of the type Boolean, "%s" given', __METHOD__, gettype($convert)), E_USER_ERROR);
		}

		$this -> convert_json = $convert;

		return $this;
	}
540225
protected function doActualConvert()
    {

        $options = $this->options;

        try {
            $im = new \Gmagick($this->source);
        } catch (\Exception $e) {
            throw new ConversionFailedException(
                'Failed creating Gmagick object of file',
                'Failed creating Gmagick object of file: "' . $this->source . '" - Gmagick threw an exception.',
                $e
            );
        }

        /*
        Seems there are currently no way to set webp options
        As noted in the following link, it should probably be done with a $im->addDefinition() method
        - but that isn't exposed (yet)
        (TODO: see if anyone has answered...)
        https://stackoverflow.com/questions/47294962/how-to-write-lossless-webp-files-with-perlmagick
        */
        // The following two does not have any effect... How to set WebP options?
        //$im->setimageoption('webp', 'webp:lossless', $options['lossless'] ? 'true' : 'false');
        //$im->setimageoption('WEBP', 'method', strval($options['method']));

        // It seems there is no COMPRESSION_WEBP...
        // http://php.net/manual/en/imagick.setimagecompression.php
        //$im->setImageCompression(Imagick::COMPRESSION_JPEG);
        //$im->setImageCompression(Imagick::COMPRESSION_UNDEFINED);



        $im->setimageformat('WEBP');

        if ($options['metadata'] == 'none') {
            // Strip metadata and profiles
            $im->stripImage();
        }

        // Ps: Imagick automatically uses same quality as source, when no quality is set
        // This feature is however not present in Gmagick
        // TODO: However, it might be possible after all - see #91
        $im->setcompressionquality($this->getCalculatedQuality());

        try {
            // We call getImageBlob().
            // That method is undocumented, but it is there!
            // - just like it is in imagick, as pointed out here:
            //   https://www.php.net/manual/ru/gmagick.readimageblob.php

            /** @scrutinizer ignore-call */
            $imageBlob = $im->getImageBlob();
        } catch (\ImagickException $e) {
            throw new ConversionFailedException(
                'Gmagick failed converting - getImageBlob() threw an exception)',
                0,
                $e
            );
        }


        //$success = $im->writeimagefile(fopen($destination, 'wb'));
        $success = @file_put_contents($this->destination, $imageBlob);

        if (!$success) {
            throw new ConversionFailedException('Failed writing file');
        } else {
            //$logger->logLn('sooms we made it!');
        }
    }
697072
public function set(TypeConverterInterface $converter): self
    {
        $serialized = $this->serialize($converter);
        $this->converters[$serialized] = $converter;

        return $this;
    }
1051621
public function convertTo(Currency $currency)
    {
        if ($this->currency->getCode() === $currency->getCode()) {
            return $this;
        }

        $baseAmount = $this->amount;

        if ($this->currency->getCode() !== static::baseCurrency()->getCode()) {
            $baseAmount = static::localAmountToDefault($this->amount, $this->currency);
        }

        return new static(
            static::baseAmountToLocal($baseAmount, $currency), $currency
        );
    }
494560
public function convertTypeDefinition(TypeDefinitionInterface $typeDefinition)
    {
        if ($typeDefinition instanceof DocumentTypeDefinitionInterface) {
            return new DocumentType($this->session, $typeDefinition);
        } elseif ($typeDefinition instanceof FolderTypeDefinitionInterface) {
            return new FolderType($this->session, $typeDefinition);
        } elseif ($typeDefinition instanceof RelationshipTypeDefinitionInterface) {
            return new RelationshipType($this->session, $typeDefinition);
        } elseif ($typeDefinition instanceof PolicyTypeDefinitionInterface) {
            return new PolicyType($this->session, $typeDefinition);
        } elseif ($typeDefinition instanceof ItemTypeDefinitionInterface) {
            return new ItemType($this->session, $typeDefinition);
        } elseif ($typeDefinition instanceof SecondaryTypeDefinitionInterface) {
            return new SecondaryType($this->session, $typeDefinition);
        } else {
            throw new CmisRuntimeException(
                sprintf('Unknown base type! Received "%s"', get_class($typeDefinition)),
                1422028427
            );
        }
    }
189831
public function convert() {
        global $CFG;
        require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php');

        // Basic/initial prevention against time/memory limits
        core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted
        raise_memory_limit(MEMORY_EXTRA);
        $this->progress->start_progress('Backup format conversion');

        if ($this->status != backup::STATUS_REQUIRE_CONV) {
            throw new restore_controller_exception('cannot_convert_not_required_status');
        }

        $this->log('backup format conversion required', backup::LOG_INFO);

        // Run conversion to the proper format
        if (!convert_helper::to_moodle2_format($this->get_tempdir(), $this->format, $this->get_logger())) {
            // todo - unable to find the conversion path, what to do now?
            // throwing the exception as a temporary solution
            throw new restore_controller_exception('unable_to_find_conversion_path');
        }

        $this->log('backup format conversion successful', backup::LOG_INFO);

        // If no exceptions were thrown, then we are in the proper format
        $this->format = backup::FORMAT_MOODLE;

        // Load plan, apply security and set status based on interactivity
        $this->load_plan();

        // Perform all initial security checks and apply (2nd param) them to settings automatically
        restore_check::check_security($this, true);

        if ($this->interactive == backup::INTERACTIVE_YES) {
            $this->set_status(backup::STATUS_SETTING_UI);
        } else {
            $this->set_status(backup::STATUS_NEED_PRECHECK);
        }
        $this->progress->end_progress();
    }
297031
public function convert() {

		// Sanity check
		if ( empty( $this->exportStylePath ) || ! is_file( $this->exportStylePath ) ) {
			$this->logError( '$this->exportStylePath must be set before calling convert().' );
			return false;
		}

		// Set logfile
		$this->logfile = $this->createTmpFile();

		// Set filename
		$filename = $this->generateFileName();
		$this->outputPath = $filename;

		// Fonts
		Container::get( 'GlobalTypography' )->getFonts();

		// CSS
		$this->truncateExportStylesheets( 'prince' );
		$timestamp = time();
		$css = $this->kneadCss();
		$css_file = \Pressbooks\Container::get( 'Sass' )->pathToUserGeneratedCss() . "/prince-$timestamp.css";
		\Pressbooks\Utility\put_contents( $css_file, $css );

		// --------------------------------------------------------------------
		// Save PDF as file in exports folder

		$configuration = \DocRaptor\Configuration::getDefaultConfiguration();
		$configuration->setUsername( DOCRAPTOR_API_KEY );

		$docraptor = new \DocRaptor\DocApi();
		$prince_options = new \DocRaptor\PrinceOptions();
		$prince_options->setNoCompress( false );
		$prince_options->setHttpTimeout( max( ini_get( 'max_execution_time' ), 30 ) );
		$prince_options->setProfile( $this->pdfProfile );
		$retval = false;

		try {
			$doc = new \DocRaptor\Doc();
			if ( defined( 'WP_TESTS_MULTISITE' ) ) {
				// Unit tests
				$document_content = str_replace( '</head>', "<style>$css</style></head>", \Pressbooks\Utility\get_contents( $this->url ) );
				$doc->setTest( true );
				$doc->setDocumentContent( $document_content );
			} elseif ( defined( 'WP_ENV' ) && ( WP_ENV === 'development' ) ) {
				// Instead of a localhost URL that DocRaptor can't see, send a document
				$response = wp_remote_get( $this->url );
				if ( is_wp_error( $response ) ) {
					$this->logError( $response->get_error_message() );
					return false;
				}
				$document_content = str_replace( '</head>', "<style>$css</style></head>", $response['body'] );
				$doc->setTest( true );
				$doc->setDocumentContent( $document_content );
			} else {
				// The real thing
				$doc->setTest( false );
				$doc->setDocumentUrl( $this->url );
			}
			$doc->setName( get_bloginfo( 'name' ) );
			$doc->setPrinceOptions( $prince_options );

			$create_response = $docraptor->createAsyncDoc( $doc );
			$done = false;
			while ( ! $done ) {
				$status_response = $docraptor->getAsyncDocStatus( $create_response->getStatusId() );
				switch ( $status_response->getStatus() ) {
					case 'completed':
						if ( ! function_exists( 'download_url' ) ) {
							require_once( ABSPATH . 'wp-admin/includes/file.php' );
						}
						$result = \download_url( $status_response->getDownloadUrl() );
						if ( is_wp_error( $result ) ) {
							$_SESSION['pb_errors'][] = __( 'Your PDF could not be retrieved.', 'pressbooks-docraptor' );
						} else {
							copy( $result, $this->outputPath );
							unlink( $result );
							$retval = true;
						}
						$done = true;
						$exportoptions = get_option( 'pressbooks_export_options' );
						if ( isset( $exportoptions['email_validation_logs'] ) && 1 === absint( $exportoptions['email_validation_logs'] ) ) {
							$msg = $this->getDetailedLog( $create_response->getStatusId() );
							\Pressbooks\Utility\put_contents( $this->logfile, $msg );
						}
						break;
					case 'failed':
						$msg = $status_response;
						\Pressbooks\Utility\put_contents( $this->logfile, $msg );
						$done = true;
						break;
					default:
						sleep( 1 );
				}
			}
		} catch ( \DocRaptor\ApiException $exception ) {
			$msg = $exception->getResponseBody();
			\Pressbooks\Utility\put_contents( $this->logfile, $msg );
		}

		if ( ! empty( $msg ) ) {
			$this->logError( \Pressbooks\Utility\get_contents( $this->logfile ) );
		}

		return $retval;
	}
11389
protected function configureConverterFromInputOptions($converter, $input)
    {
        if (!$converter instanceof ToLatexInterface) {
            throw new \InvalidArgumentException(
                'The converter used to process '
                . $input->getOption('input-format') . ' should implement the '
                . 'phpDocumentor\Plugin\Scrybe\Converter\ToPdfInterface'
            );
        }

        $converter->setTitle($input->getOption('title'));
        $converter->setAuthor($input->getOption('author'));
        $converter->setCoverLogo($input->getOption('cover-logo'));
        $converter->setTableOfContents($input->getOption('toc') !== 'false');
    }
1337818
public function set(TypeConverterInterface $converter)
    {
        $this->converters[$converter->getTypeNamespace() . ':'
            . $converter->getTypeName()] = $converter;

        return $this;
    }
1295054
function convert() {

		// Sanity check
		if ( empty( $this->exportStylePath ) || ! is_file( $this->exportStylePath ) ) {
			$this->logError( '$this->exportStylePath must be set before calling convert().' );
			return false;
		}

		// Set logfile
		$this->logfile = $this->createTmpFile();

		// Set filename
		$filename = $this->generateFileName();
		$this->outputPath = $filename;

		// Fonts
		Container::get( 'GlobalTypography' )->getFonts();

		// CSS
		$this->truncateExportStylesheets( 'prince' );
		$timestamp = time();
		$css = $this->kneadCss();
		$css_file = \Pressbooks\Container::get( 'Sass' )->pathToUserGeneratedCss() . "/prince-$timestamp.css";
		\Pressbooks\Utility\put_contents( $css_file, $css );

		// --------------------------------------------------------------------
		// Save PDF as file in exports folder

		$prince = new \PrinceXMLPhp\PrinceWrapper( PB_PRINCE_COMMAND );
		$prince->setHTML( true );
		$prince->setCompress( true );
		$prince->setHttpTimeout( max( ini_get( 'max_execution_time' ), 30 ) );
		if ( defined( 'WP_ENV' ) && ( WP_ENV === 'development' ) ) {
			$prince->setInsecure( true );
		}
		if ( $this->pdfProfile && $this->pdfOutputIntent ) {
			$prince->setPDFProfile( $this->pdfProfile );
			$prince->setPDFOutputIntent( $this->pdfOutputIntent );

		}
		$prince->addStyleSheet( $css_file );
		if ( $this->exportScriptPath ) {
			$prince->addScript( $this->exportScriptPath );
		}
		$prince->setLog( $this->logfile );
		$retval = $prince->convert_file_to_file( $this->url, $this->outputPath, $msg );

		// Prince XML is very flexible. There could be errors but Prince will still render a PDF.
		// We want to log those errors but we won't alert the user.
		if ( is_countable( $msg ) && count( $msg ) ) {
			$this->logError( \Pressbooks\Utility\get_contents( $this->logfile ), [ 'warning' => 1 ] );
		}

		return $retval;
	}
826414
public function convert(DOMDocument $document)
    {
        $xpath = new DOMXPath($document);
        $xpath->registerNamespace('docbook', 'http://docbook.org/ns/docbook');
        $xpathExpression = '//docbook:eztemplate | //docbook:eztemplateinline';

        $templates = $xpath->query($xpathExpression);
        /** @var \DOMElement[] $templatesSorted */
        $templatesSorted = array();
        $maxDepth = 0;

        foreach ($templates as $template) {
            $depth = $this->getNodeDepth($template);
            if ($depth > $maxDepth) {
                $maxDepth = $depth;
            }
            $templatesSorted[$depth][] = $template;
        }

        ksort($templatesSorted, SORT_NUMERIC);

        foreach ($templatesSorted as $templates) {
            foreach ($templates as $template) {
                $this->processTemplate($document, $template);
            }
        }

        return $document;
    }