Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/App/src/Factory/LlmsFullGeneratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@
namespace Light\App\Factory;

use Light\App\Service\LlmsFullGenerator;
use Light\Blog\Repository\PostRepository;
use Psr\Container\ContainerInterface;

use function assert;

class LlmsFullGeneratorFactory
{
public function __invoke(ContainerInterface $container): LlmsFullGenerator
{
$postRepository = $container->get(PostRepository::class);
assert($postRepository instanceof PostRepository);

$config = $container->get('config');

return new LlmsFullGenerator(
$postRepository,
$config['llms']['sourceDir'],
$config['llms']['outputFile'],
$config['application']['url'] ?? '',
Expand Down
49 changes: 34 additions & 15 deletions src/App/src/Service/LlmsFullGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Light\App\Service;

use Light\Blog\Entity\Post;
use Light\Blog\Repository\PostRepository;
use RuntimeException;

use function array_map;
Expand All @@ -15,27 +17,23 @@
use function glob;
use function implode;
use function is_file;
use function ltrim;
use function preg_replace;
use function sort;
use function sprintf;
use function str_replace;
use function trim;

/**
* Concatenates every article under the markdown source directory into a single
* plain-text file for LLM consumption, in the same format as llms.txt/llms-full.txt
* conventions (index.md first, then every other article sorted by relative path).
*
* When a pages directory is configured, the flat `*.md` files in it - the markdown
* versions of the static pages - are appended after the articles, labelled with a
* `md-pages/` prefix so their origin stays readable in the output.
* Generate llms-full.txt from index, published articles, pages
* FAQ is removed
*/
readonly class LlmsFullGenerator
{
private const SEPARATOR = '<!-- ============================================================ -->';
private const PAGES_ROOT = 'md-pages';

public function __construct(
private PostRepository $postRepository,
private string $sourceDir,
private string $outputFile,
private string $baseUrl,
Expand Down Expand Up @@ -73,19 +71,18 @@ public function write(): int
*/
private function collectEntries(): array
{
$categoryFiles = glob($this->sourceDir . '/*/*.md') ?: [];
$categoryFiles = array_map(
fn (string $path): string => ltrim(str_replace($this->sourceDir, '', $path), '/'),
$categoryFiles
$articleLabels = array_map(
fn (Post $post): string => sprintf('%s/%s.md', $post->getCategory()->getSlug(), $post->getSlug()),
$this->postRepository->getPublishedPosts()
);
sort($categoryFiles);
sort($articleLabels);

$labels = is_file($this->sourceDir . '/index.md') ? ['index.md'] : [];
$labels = array_merge($labels, $categoryFiles);
$labels = array_merge($labels, $articleLabels);

$entries = array_map(
fn (string $label): array => [
'file' => $this->sourceDir . '/' . $label,
'file' => $this->resolveArticleFile($label),
'label' => $label,
],
$labels
Expand All @@ -94,6 +91,17 @@ private function collectEntries(): array
return array_merge($entries, $this->collectPageEntries());
}

private function resolveArticleFile(string $label): string
{
$file = $this->sourceDir . '/' . $label;

if (! is_file($file)) {
throw new RuntimeException("Missing markdown file for published post: {$label}");
}

return $file;
}

/**
* The flat `*.md` files in the pages directory, appended after the articles.
*
Expand Down Expand Up @@ -125,6 +133,7 @@ private function buildSection(string $file, string $relativePath): string
}

$contents = str_replace('{{base_url}}', $this->baseUrl, $contents);
$contents = $this->stripFaqSection($contents);

return sprintf(
"%s\n<!-- %s -->\n%s\n\n%s",
Expand All @@ -134,4 +143,14 @@ private function buildSection(string $file, string $relativePath): string
trim($contents)
);
}

/**
* Removes the FAQ section, leaves the rest of the content
*/
private function stripFaqSection(string $contents): string
{
$contents = preg_replace('/^## FAQ\s*$.*?(?=^## |\z)/ms', '', $contents) ?? $contents;

return preg_replace('/\n{3,}/', "\n\n", $contents) ?? $contents;
}
}
8 changes: 7 additions & 1 deletion test/Unit/App/Factory/LlmsFullGeneratorFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Light\App\Factory\LlmsFullGeneratorFactory;
use Light\App\Service\LlmsFullGenerator;
use Light\Blog\Repository\PostRepository;
use LightTest\Unit\UnitTest;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\Exception;
Expand Down Expand Up @@ -94,7 +95,12 @@ public static function missingUrlProvider(): array
private function createContainer(array $config): ContainerInterface
{
$container = $this->createStub(ContainerInterface::class);
$container->method('get')->willReturn($config);
$container
->method('get')
->willReturnCallback(fn (string $id): mixed => match ($id) {
'config' => $config,
default => $this->createStub(PostRepository::class),
});

return $container;
}
Expand Down
Loading