<?php
declare(strict_types=1);
namespace Butschster\ContextGenerator\Document\Compiler;
use Butschster\ContextGenerator\Application\FSPath;
use Butschster\ContextGenerator\Application\Logger\LoggerPrefix;
use Butschster\ContextGenerator\Document\Compiler\Error\ErrorCollection;
use Butschster\ContextGenerator\Document\Compiler\Error\SourceError;
use Butschster\ContextGenerator\Document\Document;
use Butschster\ContextGenerator\Lib\Content\Block\TextBlock;
use Butschster\ContextGenerator\Lib\Content\ContentBuilderFactory;
use Butschster\ContextGenerator\Lib\Variable\VariableResolver;
use Butschster\ContextGenerator\Modifier\ModifiersApplier;
use Butschster\ContextGenerator\Modifier\SourceModifierRegistry;
use Butschster\ContextGenerator\SourceParserInterface;
use Psr\Log\LoggerInterface;
use Spiral\Files\FilesInterface;
/**
* Handles compiling documents
*/
final readonly class DocumentCompiler
{
/**
* @param string $basePath Base path for relative file references
* @param SourceModifierRegistry $modifierRegistry Registry for source modifiers
* @param LoggerInterface|null $logger PSR Logger instance
*/
public function __construct(
private FilesInterface $files,
private SourceParserInterface $parser,
private string $basePath,
private SourceModifierRegistry $modifierRegistry,
private VariableResolver $variables,
private ContentBuilderFactory $builderFactory = new ContentBuilderFactory(),
#[LoggerPrefix(prefix: 'document-compiler')]
private ?LoggerInterface $logger = null,
) {}
/**
* Compile a document with all its sources
*/
public function compile(Document $document): CompiledDocument
{
$outputPath = $this->variables->resolve($document->outputPath);
$this->logger?->info('Starting document compilation', [
'outputPath' => $outputPath,
]);
$errors = new ErrorCollection($document->getErrors());
$resultPath = (string) FSPath::create($this->basePath)->join($outputPath);
if (!$document->overwrite && $this->files->exists($resultPath)) {
$this->logger?->notice('Document already exists and overwrite is disabled', [
'path' => $resultPath,
]);
return new CompiledDocument(
content: '',
errors: $errors,
);
}
$this->logger?->debug('Building document content');
$compiledDocument = $this->buildContent($errors, $document);
$directory = \dirname($resultPath);
$this->logger?->debug('Ensuring directory exists', ['directory' => $directory]);
$this->files->ensureDirectory($directory);
// Add file statistics to the generated content before writing
$finalContent = $this->addFileStatistics($compiledDocument->content, $resultPath, $outputPath);
$this->logger?->debug('Writing compiled document to file', ['path' => $resultPath]);
$this->files->write($resultPath, $finalContent);
$errorCount = \count($errors);
if ($errorCount > 0) {
$this->logger?->warning('Document compiled with errors', ['errorCount' => $errorCount]);
} else {
$this->logger?->info('Document compiled successfully', [
'path' => $resultPath,
'contentLength' => \strlen($finalContent),
'fileSize' => $this->files->size($resultPath),
]);
}
return $compiledDocument->withOutputPath($this->basePath, $outputPath);
}
/**
* Build document content from all sources
*/
public function buildContent(ErrorCollection $errors, Document $document): CompiledDocument
{
$description = $this->variables->resolve($document->description);
$this->logger?->debug('Creating content builder');
$builder = $this->builderFactory->create();
$this->logger?->debug('Adding document title', ['title' => $description]);
$builder->addTitle($description);
// Add document tags if present
if ($document->hasTags()) {
$tags = \implode(', ', $document->getTags());
$this->logger?->debug('Adding document tags', ['tags' => $tags]);
$builder->addBlock(new TextBlock($tags, 'DOCUMENT_TAGS'));
}
$sources = $document->getSources();
$sourceCount = \count($sources);
$this->logger?->info('Processing document sources', [
'sourceCount' => $sourceCount,
'hasDocumentModifiers' => $document->hasModifiers(),
]);
// Create a modifiers applier with document-level modifiers if present
$modifiersApplier = ModifiersApplier::empty($this->modifierRegistry, $this->logger)
->withModifiers($document->getModifiers());
// Process all sources
foreach ($sources as $index => $source) {
$sourceType = $source::class;
$this->logger?->debug('Processing source', [
'index' => $index + 1,
'total' => $sourceCount,
'type' => $sourceType,
]);
try {
if ($source->hasDescription()) {
$description = $source->getDescription();
$this->logger?->debug('Adding source description', ['description' => $description]);
$builder->addDescription("SOURCE: {$description}");
}
$this->logger?->debug('Parsing source content');
$content = $source->parseContent($this->parser, $modifiersApplier);
$this->logger?->debug('Adding parsed content to builder', [
'contentLength' => \strlen($content),
]);
$builder->addText($content);
$this->logger?->debug('Source processed successfully');
} catch (\Throwable $e) {
$this->logger?->error('Error processing source', [
'sourceType' => $sourceType,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
$errors->add(
new SourceError(
source: $source,
exception: $e,
),
);
}
}
$this->logger?->debug('Content building completed');
// Remove empty lines at the beginning of each line
return new CompiledDocument(
content: $builder,
errors: $errors,
);
}
/**
* Add file statistics to the generated content
*
* @param string|\Stringable $content The original content
* @param string $resultPath The actual file path where content was written
* @param string $outputPath The configured output path
* @return string The content with file statistics added
*/
private function addFileStatistics(string|\Stringable $content, string $resultPath, string $outputPath): string
{
try {
// Check if file exists before attempting to read it
if (!$this->files->exists($resultPath)) {
$this->logger?->debug('File does not exist, skipping statistics', ['path' => $resultPath]);
return (string) $content;
}
$fileSize = $this->files->size($resultPath);
$fileContent = $this->files->read($resultPath);
$lineCount = \substr_count($fileContent, "\n") + 1; // Count lines including the last line
// Create a new content builder with the original content
$builder = $this->builderFactory->create();
$builder->addText((string) $content);
// Add file statistics
$this->logger?->debug('Adding file statistics', [
'fileSize' => $fileSize,
'lineCount' => $lineCount,
'filePath' => $outputPath,
]);
$builder->addFileStats($fileSize, $lineCount, $outputPath);
return $builder->build();
} catch (\Throwable $e) {
$this->logger?->warning('Failed to add file statistics', [
'path' => $resultPath,
'error' => $e->getMessage(),
]);
// Return original content if statistics calculation fails
return (string) $content;
}
}
}