diff --git a/composer.json b/composer.json index ae5f7c9..63024f9 100644 --- a/composer.json +++ b/composer.json @@ -69,6 +69,12 @@ ], "post-update-cmd": [ "@auto-scripts" + ], + "ci:test:unit": [ + "php bin/phpunit -c phpunit.xml.dist" + ], + "fix:php:cs-fixer": [ + "php-cs-fixer fix src -v --using-cache no" ] }, "conflict": { diff --git a/config/services.yaml b/config/services.yaml index 200fbf9..619b3d2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,22 +14,20 @@ parameters: assets: css: header: - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/css/theme.css' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/css/webfonts.css' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/css/fontawesome.css' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/css/theme.css' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/css/webfonts.css' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/css/fontawesome.css' footer: js: header: - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/modernizr.min.js' - footer: - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/jquery.min.js' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/underscore.min.js' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/doctools.min.js' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/popper.min.js' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/bootstrap.min.js' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/autocomplete.min.js' - - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.5.1/js/theme.min.js' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/js/jquery.min.js' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/js/underscore.min.js' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/js/doctools.min.js' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/js/popper.min.js' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/js/bootstrap.min.js' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/js/autocomplete.min.js' + - 'https://typo3.azureedge.net/typo3documentation/theme/sphinx_typo3_theme/4.9.0/js/theme.min.js' services: # default configuration for services in *this* file diff --git a/src/Command/SnippetImporter.php b/src/Command/SnippetImporter.php index 0942651..d7d8728 100644 --- a/src/Command/SnippetImporter.php +++ b/src/Command/SnippetImporter.php @@ -90,14 +90,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } - protected function importManual($manual, $input) + protected function importManual($manual, $input): void { $this->io->section('Importing ' . $this->makePathRelative( $input->getOption('rootPath'), $manual->getAbsolutePath() - ) . ' - sit tight.'); + ) . ' ...'); $this->importer->deleteManual($manual); - $this->importer->importManual($manual); } @@ -122,28 +121,32 @@ private function getManuals(InputInterface $input, OutputInterface $output): Fin return $folders; } - private function makePathRelative(string $base, string $path) + private function makePathRelative(string $base, string $path): string { - return str_replace(rtrim($base, '/') . '/', '', $path); + $normalizedBase = rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + if (str_starts_with($path, $normalizedBase)) { + return substr($path, strlen($normalizedBase)); + } + return $path; } private function formatMilliseconds(int $milliseconds): string { - $t = round($milliseconds / 1000); + $t = intdiv($milliseconds, 1000); return sprintf('%02d:%02d:%02d', (int)($t / 3600), (int)($t / 60) % 60, $t % 60); } - public function startProgress(Event $event) + public function startProgress(Event $event): void { $this->io->progressStart($event->getFiles()->count()); } - public function advanceProgress(Event $event) + public function advanceProgress(Event $event): void { $this->io->progressAdvance(); } - public function finishProgress(Event $event) + public function finishProgress(Event $event): void { $this->io->progressFinish(); } diff --git a/src/Dto/Manual.php b/src/Dto/Manual.php index 6bf02c2..372a7ae 100644 --- a/src/Dto/Manual.php +++ b/src/Dto/Manual.php @@ -69,6 +69,9 @@ public function getFilesWithSections(): Finder /** * TYPO3 Core Changelogs are treated as submanuals from typo3/cms-core manual * + * Changelogs from other packages are not treated as submanuals, because they + * can have different structure and will be threat as normal manual pages. + * * @return array */ public function getSubManuals(): array diff --git a/src/Dto/SearchDemand.php b/src/Dto/SearchDemand.php index 7770755..37c5f94 100644 --- a/src/Dto/SearchDemand.php +++ b/src/Dto/SearchDemand.php @@ -4,15 +4,15 @@ use Symfony\Component\HttpFoundation\Request; -class SearchDemand +readonly class SearchDemand { - public function __construct(protected string $query, protected int $page, protected array $filters) + public function __construct(private string $query, private int $page, private array $filters) { } public static function createFromRequest(Request $request): SearchDemand { - $requestFilters = $request->query->get('filters'); + $requestFilters = $request->query->all()['filters'] ?? []; $filters = []; if (!empty($requestFilters)) { foreach ($requestFilters as $filter => $value) { @@ -33,11 +33,8 @@ public static function createFromRequest(Request $request): SearchDemand } $page = (int)$request->query->get('page', '1'); $query = $request->query->get('q', ''); - return new self( - $query, - max($page, 1), - $filters, - ); + + return new self($query, max($page, 1), $filters); } public function getQuery(): string diff --git a/src/Helper/VersionSorter.php b/src/Helper/VersionSorter.php index ee902c1..66eb058 100644 --- a/src/Helper/VersionSorter.php +++ b/src/Helper/VersionSorter.php @@ -6,7 +6,7 @@ class VersionSorter { public static function sortVersions(array $versions, string $direction = 'asc'): array { - usort($versions, function ($a, $b) { + usort($versions, static function ($a, $b) { if ($a === 'main') { return 1; } diff --git a/src/Repository/ElasticRepository.php b/src/Repository/ElasticRepository.php index 52ddeee..c414cbf 100644 --- a/src/Repository/ElasticRepository.php +++ b/src/Repository/ElasticRepository.php @@ -159,10 +159,9 @@ public function suggest(SearchDemand $searchDemand): array $elasticaResultSet = $search->search(); $results = $elasticaResultSet->getResults(); - $out = [ + return [ 'results' => $results, ]; - return $out; } /** diff --git a/src/Service/DirectoryFinderService.php b/src/Service/DirectoryFinderService.php index 0727071..dbce5f1 100644 --- a/src/Service/DirectoryFinderService.php +++ b/src/Service/DirectoryFinderService.php @@ -6,22 +6,20 @@ class DirectoryFinderService { - public function __construct(private readonly array $allowedPaths, private readonly array $excludedDirectories) - { + public function __construct( + private readonly array $allowedPaths, + private readonly array $excludedDirectories + ) { } /** * Finds all directories containing documentation under rootPath (DOCS_ROOT_PATH) * taking into account 'allowed_paths' and 'excluded_directories' - * - * @return Finder */ public function getAllManualDirectories(string $rootPath): Finder { $allowedPathsRegexs = $this->wrapValuesWithPregDelimiters($this->allowedPaths); - - $finder = $this->getDirectoriesByPath($rootPath); - return $finder->path($allowedPathsRegexs); + return $this->getDirectoriesByPath($rootPath)->path($allowedPathsRegexs); } /** @@ -30,13 +28,13 @@ public function getAllManualDirectories(string $rootPath): Finder * * @throws \InvalidArgumentException */ - public function getDirectoriesByPath(string $docRootPath, string $packagePath=''): Finder + public function getDirectoriesByPath(string $docRootPath, string $packagePath = ''): Finder { $combinedPath = $docRootPath . ($packagePath ? '/' . $packagePath : ''); $finder = new Finder(); - // checks if given path is already a manual, as finder only checks subfolders + // If the path is a manual, use append; otherwise, set up the usual directory search if ($combinedPath !== $docRootPath && $this->objectsFileExists($combinedPath)) { $finder->append([$combinedPath]); } else { @@ -49,7 +47,7 @@ public function getDirectoriesByPath(string $docRootPath, string $packagePath='' return $finder; } - private function getFolderFilter() + private function getFolderFilter(): \Closure { $self = $this; return static function (\SplFileInfo $file) use ($self) { @@ -64,14 +62,9 @@ private function objectsFileExists(string $path): bool /** * Wraps array values with regular expression delimiters - * - * @return array */ private function wrapValuesWithPregDelimiters(array $regexs): array { - array_walk($regexs, function (&$value, $key) { - $value = '#' . $value . '#'; - }); - return $regexs; + return array_map(static fn ($value) => "#{$value}#", $regexs); } } diff --git a/src/Service/ParseDocumentationHTMLService.php b/src/Service/ParseDocumentationHTMLService.php index f3545e3..c6563b5 100644 --- a/src/Service/ParseDocumentationHTMLService.php +++ b/src/Service/ParseDocumentationHTMLService.php @@ -7,27 +7,34 @@ class ParseDocumentationHTMLService { + private bool $newRendering = true; + public function getSectionsFromFile(SplFileInfo $file): array { - return $this->getSections($file->getContents()); + $fileContents = $file->getContents(); + $crawler = new Crawler($fileContents); + $this->newRendering = $crawler->filterXPath("//meta[@name='generator' and @content='phpdocumentor/guides']")->count(); + + return $this->getSections($crawler); } - private function getSections(string $html): array + private function getSections(Crawler $html): array { - $crawler = new Crawler($html); - $sections = $crawler->filter('div[itemprop="articleBody"]'); - - if ($sections->count() === 0) { - return []; - } + $sections = $html->filter($this->newRendering ? 'article' : 'div[itemprop="articleBody"]'); - return $this->getAllSections($sections); + return $sections->count() === 0 ? [] : $this->getAllSections($sections); } + /** + * When multiple sections are present, including nested sections, + * the process iterates over each section to fetch its content snippet. + * However, child sections are excluded from this content retrieval, + * instead, they are treated as distinct sections individually. + */ private function getAllSections(Crawler $sections): array { $sectionPieces = []; - foreach ($sections->filter('div.section') as $section) { + foreach ($sections->filter($this->newRendering ? 'section' : 'div.section') as $section) { $foundHeadline = $this->findHeadline($section); if ($foundHeadline === []) { continue; @@ -56,20 +63,16 @@ private function findHeadline(\DOMElement $section): array $crawler = new Crawler($section); $headline = $crawler->filter('h1, h2, h3, h4, h5, h6')->getNode(0); - if (($headline instanceof \DOMElement) === false) { - return []; - } - - return [ + return $headline instanceof \DOMElement ? [ 'headlineText' => filter_var(htmlspecialchars($headline->textContent), FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_HIGH), 'node' => $headline, - ]; + ] : []; } private function stripSubSectionsIfAny(\DOMElement $section): \DOMElement { $crawler = new Crawler($section); - $subSections = $crawler->filter('div.section div.section'); + $subSections = $crawler->filter($this->newRendering ? 'section section' : 'div.section div.section'); if ($subSections->count() === 0) { return $section; } @@ -105,10 +108,6 @@ private function stripCodeExamples(\DOMElement $section): \DOMElement private function sanitizeString(string $input): string { - $pattern = [ - '/\s\s+/', - ]; - $regexBuildName = preg_replace($pattern, ' ', $input); - return trim($regexBuildName); + return trim(preg_replace('/\s\s+/', ' ', $input)); } } diff --git a/symfony.lock b/symfony.lock index 848c687..53c53e3 100644 --- a/symfony.lock +++ b/symfony.lock @@ -8,18 +8,6 @@ "cweagans/composer-patches": { "version": "1.7.0" }, - "doctrine/annotations": { - "version": "1.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "1.0", - "ref": "cb4152ebcadbe620ea2261da1a1c5a9b8cea7672" - } - }, - "doctrine/lexer": { - "version": "v1.0.1" - }, "elasticsearch/elasticsearch": { "version": "v5.3.0" }, diff --git a/tests/Unit/Command/SnippetImporterTest.php b/tests/Unit/Command/SnippetImporterTest.php index de4c213..d563d6f 100644 --- a/tests/Unit/Command/SnippetImporterTest.php +++ b/tests/Unit/Command/SnippetImporterTest.php @@ -26,7 +26,8 @@ public function rootPathIsUsedFromConfiguration(): void $directoryFinder = $this->prophesize(DirectoryFinderService::class); $finder = $this->prophesize(Finder::class); - $finder->hasResults()->shouldBeCalledTimes(2)->willReturn(false); + $finder->hasResults()->willReturn(true); + $finder->getIterator()->willReturn(new \AppendIterator()); $directoryFinder ->getAllManualDirectories('_docsDefault') ->shouldBeCalledTimes(1) @@ -52,8 +53,8 @@ public function rootPathCanBeDefinedViaOption(): void $directoryFinder = $this->prophesize(DirectoryFinderService::class); $finder = $this->prophesize(Finder::class); - $finder->hasResults()->shouldBeCalledTimes(2)->willReturn(false); - + $finder->hasResults()->willReturn(true); + $finder->getIterator()->willReturn(new \AppendIterator()); $directoryFinder ->getAllManualDirectories('_docsCustom') ->shouldBeCalledTimes(1) @@ -85,7 +86,7 @@ public function callsImportProcedureManualForAllReturnedManuals(): void $folder2->__toString()->willReturn('_docsFolder/c/typo3/manual-2/master/en-us'); $finder = new Finder(); - $finder->Append([$folder->reveal(), $folder2->reveal()]); + $finder->append([$folder->reveal(), $folder2->reveal()]); $importer->importManual(Argument::which('getTitle', 'typo3/manual-1'))->shouldBeCalledTimes(1); $importer->deleteManual(Argument::which('getTitle', 'typo3/manual-1'))->shouldBeCalledTimes(1); @@ -118,13 +119,13 @@ public function importsOnlyProvidedPackage(): void $folder->__toString()->willReturn('_docsFolder/c/typo3/cms-core/master/en-us'); $finder = new Finder(); - $finder->Append([$folder->reveal()]); + $finder->append([$folder->reveal()]); $directoryFinder = $this->prophesize(DirectoryFinderService::class); - $directoryFinder->getDirectoriesByPath( - '_docsDefault', - 'c/typo3/cms-core/master/en-us' - )->willReturn($finder)->shouldBeCalledTimes(1); + $directoryFinder + ->getDirectoriesByPath('_docsDefault', 'c/typo3/cms-core/master/en-us') + ->willReturn($finder) + ->shouldBeCalledTimes(1); $importer->deleteManual(Argument::which('getTitle', 'typo3/cms-core'))->shouldBeCalledTimes(1); $importer->importManual(Argument::which('getTitle', 'typo3/cms-core'))->shouldBeCalledTimes(1); diff --git a/tests/Unit/Controller/SearchControllerTest.php b/tests/Unit/Controller/SearchControllerTest.php index 57b62f6..a934f9d 100644 --- a/tests/Unit/Controller/SearchControllerTest.php +++ b/tests/Unit/Controller/SearchControllerTest.php @@ -66,7 +66,7 @@ public function searchActionAssignsQueryToTemplate(): void /** * @test */ - public function searchActionAssignsResultsToTemplate(): never + public function searchActionAssignsResultsToTemplate(): void { self::markTestIncomplete('Need to move repository to DI and replace by mock'); diff --git a/tests/Unit/Dto/ManualTest.php b/tests/Unit/Dto/ManualTest.php index b6fda6a..1a520c9 100644 --- a/tests/Unit/Dto/ManualTest.php +++ b/tests/Unit/Dto/ManualTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Unit\Dto; use App\Dto\Manual; +use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Finder\SplFileInfo; @@ -14,26 +15,100 @@ class ManualTest extends TestCase /** * @test */ - public function createFromFolder() + public function createFromFolderWithChangelog(): void { $folder = $this->prophesize(\SplFileInfo::class); - $folder->getPathname()->willReturn('_docsFolder/c/typo3/cms-core/main/en-us'); - $folder->__toString()->willReturn('_docsFolder/c/typo3/cms-core/main/en-us'); - $returnedManual = Manual::createFromFolder($folder->reveal()); + $folder->getPathname()->willReturn('_docsFolder/c/typo3/cms-core/main/en-us/Changelog/5.14'); + $folder->__toString()->willReturn('_docsFolder/c/typo3/cms-core/main/en-us/Changelog/5.14'); + + $manual = Manual::createFromFolder($folder->reveal(), true); + + $this->assertSame('_docsFolder/c/typo3/cms-core/main/en-us/Changelog/5.14', $manual->getAbsolutePath()); + $this->assertSame('typo3/cms-core-changelog', $manual->getTitle()); + $this->assertSame('Core changelog', $manual->getType()); + $this->assertSame('5.14', $manual->getVersion()); + $this->assertSame('en-us', $manual->getLanguage()); + $this->assertSame('c/typo3/cms-core/main/en-us/Changelog/5.14', $manual->getSlug()); + } + + /** + * @test + */ + public function createFromFolderWithoutChangelog(): void + { + $folder = $this->prophesize(\SplFileInfo::class); + $folder->getPathname()->willReturn('_docsFolder/c/typo3/cms-core/12.4/en-us'); + $folder->__toString()->willReturn('_docsFolder/c/typo3/cms-core/12.4/en-us'); + + $manual = Manual::createFromFolder($folder->reveal(), false); + + $this->assertSame('_docsFolder/c/typo3/cms-core/12.4/en-us', $manual->getAbsolutePath()); + $this->assertSame('typo3/cms-core', $manual->getTitle()); + $this->assertSame('System extension', $manual->getType()); + $this->assertSame('12.4', $manual->getVersion()); + $this->assertSame('en-us', $manual->getLanguage()); + $this->assertSame('c/typo3/cms-core/12.4/en-us', $manual->getSlug()); + } - self::assertInstanceOf(Manual::class, $returnedManual); - self::assertSame('_docsFolder/c/typo3/cms-core/main/en-us', $returnedManual->getAbsolutePath()); - self::assertSame('typo3/cms-core', $returnedManual->getTitle()); - self::assertSame('System extension', $returnedManual->getType()); - self::assertSame('en-us', $returnedManual->getLanguage()); - self::assertSame('c/typo3/cms-core/main/en-us', $returnedManual->getSlug()); - self::assertSame('main', $returnedManual->getVersion()); + /** + * @test + */ + public function createFromFolderWithInvalidPath(): void + { + $folder = $this->prophesize(\SplFileInfo::class); + $folder->getPathname()->willReturn('invalid/path'); + $folder->__toString()->willReturn('invalid/path'); + + $this->expectError(); + $this->expectErrorMessage('Undefined array key 2'); + + $manual = Manual::createFromFolder($folder->reveal()); + } + + public function createFromFolderWithDifferentPathTypesDataProvider(): array + { + return [ + ['_docsFolder/c/typo3/cms-core/main/en-us', 'System extension', false], + ['_docsFolder/p/vendor/package/1.0/pl-pl', 'Community extension', false], + ['_docsFolder/m/typo3/reference-coreapi/12.4/en-us', 'TYPO3 manual', false], + ['_docsFolder/c/typo3/cms-core/main/en-us/Changelog/9.4', 'Core changelog', true], + ['_docsFolder/h/typo3/docs-homepage/main/en-us', 'Docs Home Page', false] + ]; } /** * @test + * @dataProvider createFromFolderWithDifferentPathTypesDataProvider */ - public function returnsFilesWithSectionsForManual() + public function createFromFolderWithDifferentPathTypes(string $path, string $expectedType, bool $changelog = false): void + { + $folder = $this->prophesize(\SplFileInfo::class); + $folder->getPathname()->willReturn($path); + $folder->__toString()->willReturn($path); + + $manual = Manual::createFromFolder($folder->reveal(), $changelog); + + $this->assertSame($expectedType, $manual->getType()); + } + + /** + * @test + */ + public function createFromFolderWithUnmappedType(): void + { + $folder = $this->prophesize(\SplFileInfo::class); + $folder->getPathname()->willReturn('_docsFolder/x/typo3/cms-core/main/en-us'); + $folder->__toString()->willReturn('_docsFolder/x/typo3/cms-core/main/en-us'); + + $manual = Manual::createFromFolder($folder->reveal()); + + $this->assertSame('x', $manual->getType()); + } + + /** + * @test + */ + public function returnsFilesWithSectionsForManual(): void { $filesRoot = implode(DIRECTORY_SEPARATOR, [ __DIR__, @@ -53,7 +128,141 @@ public function returnsFilesWithSectionsForManual() ]; foreach ($files as $file) { /* @var $file SplFileInfo */ - self::assertTrue(in_array((string)$file, $expectedFiles), 'Unexpected file: ' . $file); + self::assertContains((string)$file, $expectedFiles, 'Unexpected file: ' . $file); } } + + /** + * @test + */ + public function subManualsAreNotReturnForNonTypo3CmsCoreExtensions(): void + { + $manual = new Manual('/path', 'Title', 'type', 'main', 'en-us', 'slug'); + + $result = $manual->getSubManuals(); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + /** + * @test + */ + public function subManualsAreNotReturnForNonMainVersion(): void + { + $manual = new Manual('/path', 'typo3/cms-core', 'type', '1.0', 'en-us', 'slug'); + + $result = $manual->getSubManuals(); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + /** + * @test + */ + public function returnsSubManuals(): void + { + $rootPath = vfsStream::setup('_docsFolder', null, [ + 'c' => [ + 'typo3' => [ + 'cms-core' => [ + 'main' => [ + 'en-us' => [ + 'Changelog' => [ + '9.4' => [ + 'index.html' => '', + ], + '10.4' => [ + 'index.html' => '', + ], + ], + 'Editor' => [ + 'index.html' => '', + ], + 'index.html' => '', + ], + ], + ], + ], + ], + ]); + + $folder = $this->prophesize(\SplFileInfo::class); + $folder->getPathname()->willReturn('_docsFolder/c/typo3/cms-core/main/en-us'); + $folder->__toString()->willReturn('_docsFolder/c/typo3/cms-core/main/en-us'); + + $manual = new Manual($rootPath->url() . '/c/typo3/cms-core/main/en-us', 'typo3/cms-core', 'System extension', 'main', 'en-us', 'c/typo3/cms-core/main/en-us'); + $subManuals = $manual->getSubManuals(); + + self::assertCount(2, $subManuals); + + self::assertSame('vfs://_docsFolder/c/typo3/cms-core/main/en-us/Changelog/9.4', $subManuals[0]->getAbsolutePath()); + self::assertSame('typo3/cms-core-changelog', $subManuals[0]->getTitle()); + self::assertSame('Core changelog', $subManuals[0]->getType()); + self::assertSame('9.4', $subManuals[0]->getVersion()); + self::assertSame('en-us', $subManuals[0]->getLanguage()); + self::assertSame('c/typo3/cms-core/main/en-us/Changelog/9.4', $subManuals[0]->getSlug()); + + self::assertSame('vfs://_docsFolder/c/typo3/cms-core/main/en-us/Changelog/10.4', $subManuals[1]->getAbsolutePath()); + self::assertSame('typo3/cms-core-changelog', $subManuals[1]->getTitle()); + self::assertSame('Core changelog', $subManuals[1]->getType()); + self::assertSame('10.4', $subManuals[1]->getVersion()); + self::assertSame('en-us', $subManuals[1]->getLanguage()); + self::assertSame('c/typo3/cms-core/main/en-us/Changelog/10.4', $subManuals[1]->getSlug()); + } + + /** + * @test + */ + public function returnsAbsolutePath(): void + { + $manual = new Manual('/path', 'Title', 'type', 'main', 'en-us', 'slug'); + $this->assertEquals('/path', $manual->getAbsolutePath()); + } + + /** + * @test + */ + public function returnsTitle(): void + { + $manual = new Manual('/path', 'Title', 'type', 'main', 'en-us', 'slug'); + $this->assertEquals('Title', $manual->getTitle()); + } + + /** + * @test + */ + public function returnsType(): void + { + $manual = new Manual('/path', 'Title', 'type', 'main', 'en-us', 'slug'); + $this->assertEquals('type', $manual->getType()); + } + + /** + * @test + */ + public function returnsVersion(): void + { + $manual = new Manual('/path', 'Title', 'type', 'main', 'en-us', 'slug'); + $this->assertEquals('main', $manual->getVersion()); + } + + /** + * @test + */ + public function returnsLanguage(): void + { + $manual = new Manual('SomePath', 'SomeTitle', 'SomeType', 'SomeVersion', 'SomeLanguage', 'SomeSlug'); + $this->assertEquals('SomeLanguage', $manual->getLanguage()); + } + + /** + * @test + */ + public function returnsSlug(): void + { + $manual = new Manual('/path', 'Title', 'type', 'main', 'en-us', 'slug'); + $this->assertEquals('slug', $manual->getSlug()); + } } diff --git a/tests/Unit/Dto/SearchDemandTest.php b/tests/Unit/Dto/SearchDemandTest.php new file mode 100644 index 0000000..6234a94 --- /dev/null +++ b/tests/Unit/Dto/SearchDemandTest.php @@ -0,0 +1,136 @@ + 'TCA', 'page' => '2', 'filters' => ['Document Type' => ['manual' => 'true']]] + ); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame('TCA', $searchDemand->getQuery()); + $this->assertSame(2, $searchDemand->getPage()); + $this->assertSame([ + 'manual_type' => ['manual'] + ], $searchDemand->getFilters()); + } + + /** + * @test + */ + public function createFromRequestWithDefaultValues(): void + { + $request = Request::create('/search'); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame('', $searchDemand->getQuery()); + $this->assertSame(1, $searchDemand->getPage()); + $this->assertSame([], $searchDemand->getFilters()); + } + + /** + * @test + */ + public function createFromRequestWithEmptyFilters(): void + { + $request = Request::create('/search', 'GET', ['filters' => []]); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame([], $searchDemand->getFilters()); + } + + /** + * @test + */ + public function createFromRequestWithMultipleFilters(): void + { + $request = Request::create( + '/search', + 'GET', + ['filters' => [ + 'Document Type' => ['manual' => 'true'], + 'Invalid Filter' => ['value' => 'true'], + 'Language' => ['en-us' => 'true', 'de-de' => 'false'], + 'Version' => ['12.4' => 'true', '11.5' => 'true'], + ]] + ); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame([ + 'manual_type' => ['manual'], + 'manual_language' => ['en-us'], + 'manual_version' => ['12.4', '11.5'], + ], $searchDemand->getFilters()); + } + + /** + * @test + */ + public function createFromRequestWithSpecialCharactersInQuery(): void + { + $request = Request::create('/search', 'GET', ['q' => 'test+query+%26+%22something+more%22']); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame('test+query+%26+%22something+more%22', $searchDemand->getQuery()); + } + + /** + * @test + */ + public function createFromRequestWithInvalidFilter(): void + { + $request = Request::create( + '/search', + 'GET', + ['filters' => ['Invalid Filter' => ['value' => 'true']]] + ); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame([], $searchDemand->getFilters()); + } + + /** + * @test + */ + public function createFromRequestWithNegativePageNumber(): void + { + $request = Request::create('/search', 'GET', ['page' => -1]); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame(1, $searchDemand->getPage()); + } + + /** + * @test + */ + public function createFromRequestWithPageNumberAsString(): void + { + $request = Request::create('/search', 'GET', ['page' => '3']); + + $searchDemand = SearchDemand::createFromRequest($request); + + $this->assertSame(3, $searchDemand->getPage()); + } +} diff --git a/tests/Unit/Helper/VersionSorterTest.php b/tests/Unit/Helper/VersionSorterTest.php index db2cd85..4785a3f 100644 --- a/tests/Unit/Helper/VersionSorterTest.php +++ b/tests/Unit/Helper/VersionSorterTest.php @@ -11,13 +11,13 @@ class VersionSorterTest extends TestCase * @test * @dataProvider sortVersionsDataProvider */ - public function sortVersions($versions, $direction, $expected) + public function sortVersions($versions, $direction, $expected): void { $actual = VersionSorter::sortVersions($versions, $direction); self::assertEquals($expected, $actual); } - public function sortVersionsDataProvider() + public function sortVersionsDataProvider(): array { return [ [ diff --git a/tests/Unit/Service/DirectoryFinderServiceTest.php b/tests/Unit/Service/DirectoryFinderServiceTest.php index c6bc6d8..c34e48b 100644 --- a/tests/Unit/Service/DirectoryFinderServiceTest.php +++ b/tests/Unit/Service/DirectoryFinderServiceTest.php @@ -11,7 +11,7 @@ class DirectoryFinderServiceTest extends TestCase /** * @test */ - public function returnsManualsFromFolder() + public function returnsManualsFromFolder(): void { $subject = new DirectoryFinderService(['^m/', '^c/', '^p/'], ['other', 'draft', 'typo3cms/extensions']); @@ -66,4 +66,385 @@ public function returnsManualsFromFolder() self::assertCount(6, $subject->getAllManualDirectories($rootDir->url())); } + + public function getAllManualDirectoriesRespectsOnlyDirectoriesWithMetadataFileDataProvider(): array + { + return [ + 'none directory contain metadata file' => [ + 'allowedPathsRegexs' => ['^p/'], + 'excluded' => [], + [ + 'p' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + ], + ], + ], + 'gallery' => [ + 'main' => [ + 'en-us' => [ + ], + ], + ], + 'logger' => [ + 'main' => [ + 'en-us' => [ + ], + ], + ], + ], + ], + ], + 'expected' => 0, + ], + 'single directory contains metadata file' => [ + 'allowedPathsRegexs' => ['^p/'], + 'excluded' => [], + [ + 'p' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + ], + ], + ], + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'logger' => [ + 'main' => [ + 'en-us' => [ + ], + ], + ], + ], + ], + ], + 'expected' => 1, + ], + 'all directories contain metadata file' => [ + 'allowedPathsRegexs' => ['^p/'], + 'excluded' => [], + [ + 'p' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'logger' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + ], + 'expected' => 3, + ], + ]; + } + + /** + * @test + * @dataProvider getAllManualDirectoriesRespectsOnlyDirectoriesWithMetadataFileDataProvider + */ + public function getAllManualDirectoriesRespectsOnlyDirectoriesWithMetadataFile( + array $allowedPathsRegexs, + array $excludedDirectories, + array $foldersStructure, + int $expectedDirectories + ): void + { + $vfsStream = vfsStream::setup('_docsFolder', null, $foldersStructure); + $subject = new DirectoryFinderService($allowedPathsRegexs, $excludedDirectories); + + $finder = $subject->getAllManualDirectories($vfsStream->url()); + + self::assertEquals($expectedDirectories, iterator_count($finder)); + } + + public function getAllManualDirectoriesRespectsAllowedPathsDataProvider(): array + { + return [ + 'all paths are allowed (no selection)' => [ + 'allowedPathsRegexs' => [], + 'excluded' => [], + [ + 'c' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + 'm' => [ + 'vendor' => [ + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + 'p' => [ + 'vendor' => [ + 'logger' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + ], + 'expected' => 3, + ], + '1 out of 3 paths are allowed' => [ + 'allowedPathsRegexs' => ['^c/'], + 'excluded' => [], + [ + 'c' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + 'm' => [ + 'vendor' => [ + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + 'p' => [ + 'vendor' => [ + 'logger' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + ], + 'expected' => 1, + ], + '2 out of 3 paths are allowed' => [ + 'allowedPathsRegexs' => ['^c/', '^p/'], + 'excluded' => [], + [ + 'c' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + 'm' => [ + 'vendor' => [ + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + 'p' => [ + 'vendor' => [ + 'logger' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + ], + 'expected' => 2, + ], + ]; + } + + /** + * @test + * @dataProvider getAllManualDirectoriesRespectsAllowedPathsDataProvider + */ + public function getAllManualDirectoriesRespectsAllowedPaths( + array $allowedPathsRegexs, + array $excludedDirectories, + array $foldersStructure, + int $expectedDirectories + ): void + { + $vfsStream = vfsStream::setup('_docsFolder', null, $foldersStructure); + $subject = new DirectoryFinderService($allowedPathsRegexs, $excludedDirectories); + + $finder = $subject->getAllManualDirectories($vfsStream->url()); + + self::assertEquals($expectedDirectories, iterator_count($finder)); + } + + public function getDirectoriesByPathRespectsDirectoriesExclusionDataProvider(): array + { + return [ + 'all excluded' => [ + 'allowedPathsRegexs' => ['^p/'], + 'excluded' => ['news', 'gallery', 'logger'], + [ + 'p' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'logger' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + ], + 'expected' => 0, + ], + 'single excluded' => [ + 'allowedPathsRegexs' => ['^p/'], + 'excluded' => ['news'], + [ + 'p' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'logger' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + ], + 'expected' => 2, + ], + 'none excluded' => [ + 'allowedPathsRegexs' => ['^p/'], + 'excluded' => [], + [ + 'p' => [ + 'vendor' => [ + 'news' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'gallery' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + 'logger' => [ + 'main' => [ + 'en-us' => [ + 'objects.inv.json' => '' + ], + ], + ], + ], + ], + ], + 'expected' => 3, + ], + ]; + } + + /** + * @test + * @dataProvider getDirectoriesByPathRespectsDirectoriesExclusionDataProvider + */ + public function getDirectoriesByPathRespectsDirectoriesExclusion( + array $allowedPathsRegexs, + array $excludedDirectories, + array $foldersStructure, + int $expectedDirectories + ): void { + $vfsStream = vfsStream::setup('_docsFolder', null, $foldersStructure); + $subject = new DirectoryFinderService($allowedPathsRegexs, $excludedDirectories); + + $finder = $subject->getDirectoriesByPath($vfsStream->url()); + + self::assertCount($expectedDirectories, $finder->directories()); + } } diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/MultipleUniqueHeadlines.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/MultipleUniqueHeadlines.html new file mode 100644 index 0000000..1576011 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/MultipleUniqueHeadlines.html @@ -0,0 +1,22 @@ + +
+
+

Headline 1

+
+
+

Headline 2

+
+
+

Headline 3

+
+
+

Headline 4

+
+
+
Headline 5
+
+
+
Headline 6
+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SectionsWithHeadlinesNotAsFirstContentElement.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SectionsWithHeadlinesNotAsFirstContentElement.html new file mode 100644 index 0000000..b3c95e1 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SectionsWithHeadlinesNotAsFirstContentElement.html @@ -0,0 +1,25 @@ + +
+
+
Section 1 content
+

Section 1

+
+
+

Section 2 content part 1

+

Section 2

+

Section 2 content part 2

+
+
+

Section 3 content part 1

+

Section 3 content part 2

+

Section 3

+
+
+

Section 4 content part 1

+

Section 4 content part 2

+

Section 4

+

Section 4 content part 3

+
Headline 5
+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SectionsWithoutHeadlines.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SectionsWithoutHeadlines.html new file mode 100644 index 0000000..853df6a --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SectionsWithoutHeadlines.html @@ -0,0 +1,18 @@ + +
+
+
Section 1 content
+
+
+
Section 2 content
+
+
+

Headline 3

+
Section 3 content
+
+
+
Section 4 content
+

Headline 4

+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SingleSectionWithMultipleHeadlines.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SingleSectionWithMultipleHeadlines.html new file mode 100644 index 0000000..a42e594 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/LegacyStructure/SingleSectionWithMultipleHeadlines.html @@ -0,0 +1,12 @@ + +
+
+

Headline 1

+

Headline 2

+

Headline 3

+

Headline 4

+
Headline 5
+
Headline 6
+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/MultipleUniqueHeadlines.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/MultipleUniqueHeadlines.html new file mode 100644 index 0000000..3a09f26 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/MultipleUniqueHeadlines.html @@ -0,0 +1,25 @@ + + + + +
+
+

Headline 1

+
+
+

Headline 2

+
+
+

Headline 3

+
+
+

Headline 4

+
+
+
Headline 5
+
+
+
Headline 6
+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SectionsWithHeadlinesNotAsFirstContentElement.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SectionsWithHeadlinesNotAsFirstContentElement.html new file mode 100644 index 0000000..dbae7c5 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SectionsWithHeadlinesNotAsFirstContentElement.html @@ -0,0 +1,28 @@ + + + + +
+
+
Section 1 content
+

Section 1

+
+
+

Section 2 content part 1

+

Section 2

+

Section 2 content part 2

+
+
+

Section 3 content part 1

+

Section 3 content part 2

+

Section 3

+
+
+

Section 4 content part 1

+

Section 4 content part 2

+

Section 4

+

Section 4 content part 3

+
Headline 5
+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SectionsWithoutHeadlines.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SectionsWithoutHeadlines.html new file mode 100644 index 0000000..53f30c3 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SectionsWithoutHeadlines.html @@ -0,0 +1,21 @@ + + + + +
+
+
Section 1 content
+
+
+
Section 2 content
+
+
+

Headline 3

+
Section 3 content
+
+
+
Section 4 content
+

Headline 4

+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SingleSectionWithMultipleHeadlines.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SingleSectionWithMultipleHeadlines.html new file mode 100644 index 0000000..faf315c --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsContentElements/SingleSectionWithMultipleHeadlines.html @@ -0,0 +1,15 @@ + + + + +
+
+

Headline 1

+

Headline 2

+

Headline 3

+

Headline 4

+
Headline 5
+
Headline 6
+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithCodeExamples.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithCodeExamples.html new file mode 100644 index 0000000..49e5b82 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithCodeExamples.html @@ -0,0 +1,200 @@ + +
+
+ + + + + +
+
+ +
+

Rendering Page Trees

+

In your backend modules you might like to show information or perform +processing for a part of the page tree. There is a whole family of +libraries in the core for making trees from records, static page trees +or page trees that can be browsed (open/close nodes).

+

This simple example demonstrates how to produce the HTML for a static +page tree. The result looks like:

+
+A page tree +

A static page tree in TYPO3 Backend

+
+

The tree object itself is prepared this way (taken from +EXT:examples/Classes/Controller/DefaultController.php):

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
public function treeAction() {
+   // Get page record for tree starting point
+   $startingPoint = 1;
+   $pageRecord = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord(
+      'pages',
+      $startingPoint
+   );
+
+   // Create and initialize the tree object
+   /** @var $tree \TYPO3\CMS\Backend\Tree\View\PageTreeView */
+   $tree = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\View\PageTreeView::class);
+   $tree->init('AND ' . $GLOBALS['BE_USER']->getPagePermsClause(1));
+
+   // Creating the icon for the current page and add it to the tree
+   $html = \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIconForRecord(
+      'pages',
+      $pageRecord,
+      array(
+         'title' => $pageRecord['title']
+      )
+   );
+   $tree->tree[] = array(
+       'row' => $pageRecord,
+       'HTML' => $html
+   );
+
+   // Create the page tree, from the starting point, 2 levels deep
+   $depth = 2;
+   $tree->getTree(
+      $startingPoint,
+      $depth,
+      ''
+   );
+
+   // Pass the tree to the view
+   $this->view->assign(
+      'tree',
+      $tree->tree
+   );
+}
+
+
+
    +
  • At the top of the code we define the starting page and get the corresponding +page record using \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord().
  • +
  • Next we create an instance of \TYPO3\CMS\Backend\Tree\View\PageTreeView, +which we use for generating the tree. Notice how the BE_USER object is +called to get a SQL where clause that will ensure that only pages +that are accessible for the user will be shown in the tree!
  • +
  • As a next step we manually add the starting page to the page tree. +This is not done automatically because it is not always a desirable +behavior. Note the use of \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIconForRecord() +to fetch the right icon for the page.
  • +
  • Finally we get the tree to prepare itself, up to a certain depth. +Internally this will - in particular - generate a HTML part containing +the tree elements and the page icon itself.
  • +
  • The rendered page tree is stored in a data array inside of the tree +object. We need to traverse the tree data to create the tree in HTML. +This gives us the chance to organize the tree in a table for instance. +It is this part that we pass on to the view.
  • +
+

The result is rendered with a very simple Fluid template:

+
<f:for each="{tree}" as="page">
+   <tr class="db_list_normal">
+      <td>{page.depthData -> f:format.raw()}<f:format.raw>{page.HTML}</f:format.raw> {page.row.title}</td>
+      <td>{page.row.uid}</td>
+   </tr>
+</f:for>
+
+
+

We do a simple loop on the tree array of pages and display the relevant +elements.

+
+ +
+
+ + +
+
+ + + + diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithSubSections.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithSubSections.html new file mode 100644 index 0000000..e9e0aff --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithSubSections.html @@ -0,0 +1,34 @@ +
+
+ +
+

Deprecation: #88839 - CLI lowlevel request handlers

+

See Issue #88839

+
+

Description

+

The interface \TYPO3\CMS\Core\Console\RequestHandlerInterface +and the class \TYPO3\CMS\Core\Console\CommandRequestHandler have been introduced in TYPO3 v7 to streamline +various entry points for CLI-related functionality. Back then, there were Extbase command requests and +CommandLineController entry points.

+

With TYPO3 v10, the only way to handle CLI commands is via the \TYPO3\CMS\Core\Console\CommandApplication class which is +a wrapper around Symfony Console. All logic is now located in the Application, and thus, the interface and +the class have been marked as deprecated.

+
+
+

Impact

+

When instantiating the CLI \TYPO3\CMS\Core\Console\RequestHandler class, +a PHP E_USER_DEPRECATED error will be triggered.

+
+
+

Affected Installations

+

Any TYPO3 installation having custom CLI request handlers wrapped via the interface or extending the +CLI request handler class.

+
+
+

Migration

+

Switch to a Symfony Command or provide a custom CLI entry point.

+
+
+ +
+
diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithSubSectionsSmall.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithSubSectionsSmall.html new file mode 100644 index 0000000..7fa8185 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MarkupWithSubSectionsSmall.html @@ -0,0 +1,22 @@ +
+ +
+

Features and Basic Concept

+

The main goal for this blog extension was to use TYPO3s core concepts and elements to + provide a + full-blown blog that + users of TYPO3 can instantly understand and use.

+
+

Pages as blog entries

+

Blog entries are simply pages with a special page type blog entry and can be created + and + edited via the well-known page + module. Creating new entries is as simple as dragging a new entry into the page + tree.

+
+
+ + +
diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MultiByteMarkupWithFullLayout.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MultiByteMarkupWithFullLayout.html new file mode 100644 index 0000000..145095e --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/MultiByteMarkupWithFullLayout.html @@ -0,0 +1,3400 @@ + + + + + + + + + + + + + + Important: #100658 - Drop use TSconfig options createFoldersInEB and folderTree.hideCreateFolder — Core main documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ +
+
+
+ + + + + + + + +
+
+

Display settings

+
+ Color scheme of code blocks:
+ + + +
+
+
+
+ + +
+
+ +
+
+ +
+
+ +
+

Important: #100658 - Drop use TSconfig options createFoldersInEB and folderTree.hideCreateFolder

+

See forge#100658

+
+

Description

+

The user TSconfig options createFoldersInEB and folderTree.hideCreateFolder were + used in the past to control the existence of the "Create folder" form in Element + Browser instances. With the migration of the "Create folder" view into a separate + modal used in EXT:filelist, which is based on Element Browser as well, those + options became useless and are therefore dropped.

+
+
+ +
+
+ + + +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/SimpleMarkup.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/SimpleMarkup.html new file mode 100644 index 0000000..695fff8 --- /dev/null +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/LegacyStructure/SimpleMarkup.html @@ -0,0 +1,8 @@ + +
+
+

Headline 1

+

Content 1

+
+
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithCodeExamples.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithCodeExamples.html index 49e5b82..c51438b 100644 --- a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithCodeExamples.html +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithCodeExamples.html @@ -1,192 +1,332 @@ + + + + + + + + + + + + + + +Rendering Page Trees — TYPO3 Explained 10.4 documentation + + + + + + + + + + + + + + +
+
+
+
+ + +
+
+ +
+
+ +
+ +
+
+
+ +
-
- - - - + + +
+
- + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSections.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSections.html index e9e0aff..df47048 100644 --- a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSections.html +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSections.html @@ -1,10 +1,15 @@ + + + +
+
-
+

Deprecation: #88839 - CLI lowlevel request handlers

See Issue #88839

-
+

Description

The interface \TYPO3\CMS\Core\Console\RequestHandlerInterface and the class \TYPO3\CMS\Core\Console\CommandRequestHandler have been introduced in TYPO3 v7 to streamline @@ -13,22 +18,25 @@

Description\TYPO3\CMS\Core\Console\CommandApplication class which is a wrapper around Symfony Console. All logic is now located in the Application, and thus, the interface and the class have been marked as deprecated.

-

-
+
+

Impact

When instantiating the CLI \TYPO3\CMS\Core\Console\RequestHandler class, a PHP E_USER_DEPRECATED error will be triggered.

-
-
+ +

Affected Installations

Any TYPO3 installation having custom CLI request handlers wrapped via the interface or extending the CLI request handler class.

-
-
+ +

Migration

Switch to a Symfony Command or provide a custom CLI entry point.

-
-
+ +
+ +
+ diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSectionsSmall.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSectionsSmall.html index 7fa8185..5585dab 100644 --- a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSectionsSmall.html +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MarkupWithSubSectionsSmall.html @@ -1,22 +1,29 @@ -
+ + + + +
+
-
-

Features and Basic Concept

-

The main goal for this blog extension was to use TYPO3s core concepts and elements to - provide a - full-blown blog that - users of TYPO3 can instantly understand and use.

-
-

Pages as blog entries

-

Blog entries are simply pages with a special page type blog entry and can be created - and - edited via the well-known page - module. Creating new entries is as simple as dragging a new entry into the page - tree.

-
-
+
+

Features and Basic Concept

+

The main goal for this blog extension was to use TYPO3s core concepts and elements to + provide a + full-blown blog that + users of TYPO3 can instantly understand and use.

+
+

Pages as blog entries

+

Blog entries are simply pages with a special page type blog entry and can be created + and + edited via the well-known page + module. Creating new entries is as simple as dragging a new entry into the page + tree.

+
+
-
+
+ + \ No newline at end of file diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MultiByteMarkupWithFullLayout.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MultiByteMarkupWithFullLayout.html index a48937f..fdf9715 100644 --- a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MultiByteMarkupWithFullLayout.html +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/MultiByteMarkupWithFullLayout.html @@ -1,2231 +1,269 @@ - - + - - - - - - - - - Feature: #69572 - Page module Notice “Content is also shown on:” — TYPO3 CMS Core ChangeLog latest (10-dev) documentation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + Important: #100658 - Drop use TSconfig options createFoldersInEB and folderTree.hideCreateFolder — Core main (development) documentation + + + + + + + + + + + + + - - - -
- - - - -
- - - - - - -
-
-
- - -
- - - Next - - - - - - - Previous - - - -
-
-
- -
-

Feature: #69572 - Page module Notice “Content is also shown on:”

-

See Issue #69572

-
-

Description

-

When page content is inherited from a different page via “Show content from page” there is a notice displayed on the page that is pulling in content from a different page.

-

As of now, the page whose content is used on other pages gets an info box that indicates which other pages use these contents.

-
-
-

Impact

-

On pages that are inherited elsewhere you see a notice which links to the pages where the content is inherited.

-
-
- - -
-
- + +
+
+
+
+ + +
+
+ +
-
- -
- -
- - - - - - - - - - - - - - - + + + +
+
+
+ +
+
+
+ +
+
+ + + +
+

Important: #100658 - Drop use TSconfig options createFoldersInEB and folderTree.hideCreateFolder

+ + +

See forge#100658

+
+

Description

+ +

The user TSconfig options createFoldersInEB and folderTree.hideCreateFolder were + used in the past to control the existence of the "Create folder" form in Element + Browser instances. With the migration of the "Create folder" view into a separate + modal used in EXT:filelist, which is based on Element Browser as well, those + options became useless and are therefore dropped.

+
+ +
+ + +
+
+ + +
+
+
+
+ + + + + + + - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/SimpleMarkup.html b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/SimpleMarkup.html index 695fff8..1000306 100644 --- a/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/SimpleMarkup.html +++ b/tests/Unit/Service/Fixtures/ParseDocumentationHTMLServiceTest/ReturnsSectionsFromFile/SimpleMarkup.html @@ -1,8 +1,13 @@ + + + +
-
+

Headline 1

Content 1

-
+
+
diff --git a/tests/Unit/Service/ImportManualHTMLServiceTest.php b/tests/Unit/Service/ImportManualHTMLServiceTest.php index 435e5fb..4fa33cd 100644 --- a/tests/Unit/Service/ImportManualHTMLServiceTest.php +++ b/tests/Unit/Service/ImportManualHTMLServiceTest.php @@ -24,7 +24,7 @@ class ImportManualHTMLServiceTest extends TestCase /** * @test */ - public function allowsToDeleteManual() + public function allowsToDeleteManual(): void { $manual = $this->prophesize(Manual::class)->reveal(); $repo = $this->prophesize(ElasticRepository::class); diff --git a/tests/Unit/Service/ParseDocumentationHTMLServiceTest.php b/tests/Unit/Service/ParseDocumentationHTMLServiceTest.php index 332e443..9f47a0c 100644 --- a/tests/Unit/Service/ParseDocumentationHTMLServiceTest.php +++ b/tests/Unit/Service/ParseDocumentationHTMLServiceTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Unit\Service; use App\Service\ParseDocumentationHTMLService; +use Exception; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Finder\SplFileInfo; @@ -13,18 +14,278 @@ class ParseDocumentationHTMLServiceTest extends TestCase /** * @test - * @dataProvider documentationDataProvider + * @throws Exception */ - public function returnsSectionsFromFile(string $relativeFileName, array $expectedResult) + public function returnsMultipleUniqueHeadlinesForLegacyStructure(): void { - $fixtureFile = implode(DIRECTORY_SEPARATOR, [ - __DIR__, - 'Fixtures', - 'ParseDocumentationHTMLServiceTest', - 'ReturnsSectionsFromFile', - ucfirst($this->dataName()) . '.html', - ]); - $fileContent = file_get_contents($fixtureFile); + $fileContent = $this->getFixtureFileContents('ReturnsContentElements/LegacyStructure', 'MultipleUniqueHeadlines'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'headline-1-section', + 'snippet_title' => 'Headline 1', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-2-section', + 'snippet_title' => 'Headline 2', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-3-section', + 'snippet_title' => 'Headline 3', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-4-section', + 'snippet_title' => 'Headline 4', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-5-section', + 'snippet_title' => 'Headline 5', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-6-section', + 'snippet_title' => 'Headline 6', + 'snippet_content' => '', + ], + ], $receivedSection); + } + + /** + * @test + * @throws Exception + */ + public function treatsAllHeadlinesInsideSectionAsSnippetContentExceptTheH1ForLegacyStructure(): void + { + $fileContent = $this->getFixtureFileContents('ReturnsContentElements/LegacyStructure', 'SingleSectionWithMultipleHeadlines'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'single-section', + 'snippet_title' => 'Headline 1', + 'snippet_content' => 'Headline 2 Headline 3 Headline 4 Headline 5 Headline 6', + ], + ], $receivedSection); + } + + /** + * @test + * @throws Exception + */ + public function sectionsWithoutHeadlineAreOmittedForLegacyStructure(): void + { + $fileContent = $this->getFixtureFileContents('ReturnsContentElements/LegacyStructure', 'SectionsWithoutHeadlines'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'section-3', + 'snippet_title' => 'Headline 3', + 'snippet_content' => 'Section 3 content', + ], + [ + 'fragment' => 'section-4', + 'snippet_title' => 'Headline 4', + 'snippet_content' => 'Section 4 content', + ], + ], $receivedSection); + } + + /** + * @test + * @throws Exception + */ + public function sectionsWithHeadlinesNotAsFirstContentElementForLegacyStructure(): void + { + $fileContent = $this->getFixtureFileContents('ReturnsContentElements/LegacyStructure', 'SectionsWithHeadlinesNotAsFirstContentElement'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'section-1', + 'snippet_title' => 'Section 1', + 'snippet_content' => 'Section 1 content', + ], + [ + 'fragment' => 'section-2', + 'snippet_title' => 'Section 2', + 'snippet_content' => 'Section 2 content part 1 Section 2 content part 2', + ], + [ + 'fragment' => 'section-3', + 'snippet_title' => 'Section 3', + 'snippet_content' => 'Section 3 content part 1 Section 3 content part 2', + ], + [ + 'fragment' => 'section-4', + 'snippet_title' => 'Section 4', + 'snippet_content' => 'Section 4 content part 1 Section 4 content part 2 Section 4 content part 3 Headline 5', + ], + ], $receivedSection); + } + + /** + * @test + * @throws Exception + */ + public function returnsMultipleUniqueHeadlines(): void + { + $fileContent = $this->getFixtureFileContents('ReturnsContentElements', 'MultipleUniqueHeadlines'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'headline-1-section', + 'snippet_title' => 'Headline 1', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-2-section', + 'snippet_title' => 'Headline 2', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-3-section', + 'snippet_title' => 'Headline 3', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-4-section', + 'snippet_title' => 'Headline 4', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-5-section', + 'snippet_title' => 'Headline 5', + 'snippet_content' => '', + ], + [ + 'fragment' => 'headline-6-section', + 'snippet_title' => 'Headline 6', + 'snippet_content' => '', + ], + ], $receivedSection); + } + + /** + * @test + * @throws Exception + */ + public function treatsAllHeadlinesInsideSectionAsSnippetContentExceptTheH1(): void + { + $fileContent = $this->getFixtureFileContents('ReturnsContentElements', 'SingleSectionWithMultipleHeadlines'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'single-section', + 'snippet_title' => 'Headline 1', + 'snippet_content' => 'Headline 2 Headline 3 Headline 4 Headline 5 Headline 6', + ], + ], $receivedSection); + } + + /** + * @test + * @throws Exception + */ + public function sectionsWithoutHeadlineAreOmitted(): void + { + $fileContent = $this->getFixtureFileContents('ReturnsContentElements', 'SectionsWithoutHeadlines'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'section-3', + 'snippet_title' => 'Headline 3', + 'snippet_content' => 'Section 3 content', + ], + [ + 'fragment' => 'section-4', + 'snippet_title' => 'Headline 4', + 'snippet_content' => 'Section 4 content', + ], + ], $receivedSection); + } + + /** + * @test + * @throws Exception + */ + public function sectionsWithHeadlinesNotAsFirstContentElement(): void + { + $fileContent = $this->getFixtureFileContents('ReturnsContentElements', 'SectionsWithHeadlinesNotAsFirstContentElement'); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + self::assertEquals([ + [ + 'fragment' => 'section-1', + 'snippet_title' => 'Section 1', + 'snippet_content' => 'Section 1 content', + ], + [ + 'fragment' => 'section-2', + 'snippet_title' => 'Section 2', + 'snippet_content' => 'Section 2 content part 1 Section 2 content part 2', + ], + [ + 'fragment' => 'section-3', + 'snippet_title' => 'Section 3', + 'snippet_content' => 'Section 3 content part 1 Section 3 content part 2', + ], + [ + 'fragment' => 'section-4', + 'snippet_title' => 'Section 4', + 'snippet_content' => 'Section 4 content part 1 Section 4 content part 2 Section 4 content part 3 Headline 5', + ], + ], $receivedSection); + } + + /** + * @test + * @dataProvider documentationDataProviderForLegacyStructure + * @throws Exception + */ + public function returnsSectionsFromFileForLegacyStructure(array $expectedResult): void + { + $fileContent = $this->getFixtureFileContents('ReturnsSectionsFromFile/LegacyStructure', ucfirst($this->dataName())); $file = $this->prophesize(SplFileInfo::class); $file->getContents()->willReturn($fileContent); $subject = new ParseDocumentationHTMLService(); @@ -37,11 +298,10 @@ public function returnsSectionsFromFile(string $relativeFileName, array $expecte self::assertCount(count($expectedResult), $receivedSection, 'Did not receive expected number of sections.'); } - public function documentationDataProvider() + public function documentationDataProviderForLegacyStructure(): array { return [ 'simpleMarkup' => [ - 'relativeFileName' => 'p/docsearch/blog/8.7/en-us', 'expectedResult' => [ [ 'fragment' => 'first-section', @@ -51,30 +311,20 @@ public function documentationDataProvider() ] ], 'multiByteMarkupWithFullLayout' => [ - 'relativeFileName' => 'p/docsearch/blog/8.7/en-us', 'expectedResult' => [ [ - 'fragment' => 'feature-69572-page-module-notice-content-is-also-shown-on', - 'snippet_title' => 'Feature: #69572 - Page module Notice Content is also shown on:', - 'snippet_content' => 'See Issue #69572', + 'fragment' => 'important-100658-drop-use-tsconfig-options-createfoldersineb-and-foldertree-hidecreatefolder', + 'snippet_title' => 'Important: #100658 - Drop use TSconfig options createFoldersInEB and folderTree.hideCreateFolder', + 'snippet_content' => 'See forge#100658', ], [ 'fragment' => 'description', 'snippet_title' => 'Description', - 'snippet_content' => implode("\n", [ - 'When page content is inherited from a different page via “Show content from page” there is a notice displayed on the page that is pulling in content from a different page.', - 'As of now, the page whose content is used on other pages gets an info box that indicates which other pages use these contents.', - ]), - ], - [ - 'fragment' => 'impact', - 'snippet_title' => 'Impact', - 'snippet_content' => 'On pages that are inherited elsewhere you see a notice which links to the pages where the content is inherited.', + 'snippet_content' => 'The user TSconfig options createFoldersInEB and folderTree.hideCreateFolder were used in the past to control the existence of the "Create folder" form in Element Browser instances. With the migration of the "Create folder" view into a separate modal used in EXT:filelist, which is based on Element Browser as well, those options became useless and are therefore dropped.', ], ], ], 'markupWithSubSections' => [ - 'relativeFileName' => 'p/docsearch/blog/8.7/en-us', 'expected' => [ [ 'fragment' => 'deprecation-88839-cli-lowlevel-request-handlers', @@ -118,7 +368,6 @@ public function documentationDataProvider() ] ], 'markupWithSubSectionsSmall' => [ - 'relativeFileName' => 'p/docsearch/blog/8.7/en-us', 'expected' => [ [ 'fragment' => 'features-and-basic-concept', @@ -133,7 +382,6 @@ public function documentationDataProvider() ] ], 'markupWithCodeExamples' => [ - 'relativeFileName' => 'p/docsearch/blog/8.7/en-us', 'expected' => [ [ 'fragment' => 'rendering-page-trees', @@ -167,4 +415,158 @@ public function documentationDataProvider() ], ]; } + + /** + * @test + * @dataProvider documentationDataProvider + * @throws Exception + */ + public function returnsSectionsFromFile(array $expectedResult): void + { + $fileContent = $this->getFixtureFileContents('ReturnsSectionsFromFile', ucfirst($this->dataName())); + $file = $this->prophesize(SplFileInfo::class); + $file->getContents()->willReturn($fileContent); + $subject = new ParseDocumentationHTMLService(); + $receivedSection = $subject->getSectionsFromFile($file->reveal()); + + foreach ($expectedResult as $sectionIndex => $expectedSection) { + self::assertSame($receivedSection[$sectionIndex], $expectedSection, 'Section with index ' . $sectionIndex . ' did not match.'); + } + + self::assertCount(count($expectedResult), $receivedSection, 'Did not receive expected number of sections.'); + } + + public function documentationDataProvider(): array + { + return [ + 'simpleMarkup' => [ + 'expectedResult' => [ + [ + 'fragment' => 'first-section', + 'snippet_title' => 'Headline 1', + 'snippet_content' => 'Content 1', + ] + ] + ], + 'multiByteMarkupWithFullLayout' => [ + 'expectedResult' => [ + [ + 'fragment' => 'important-100658-drop-use-tsconfig-options-createfoldersineb-and-foldertree-hidecreatefolder', + 'snippet_title' => 'Important: #100658 - Drop use TSconfig options createFoldersInEB and folderTree.hideCreateFolder', + 'snippet_content' => 'See forge#100658', + ], + [ + 'fragment' => 'description', + 'snippet_title' => 'Description', + 'snippet_content' => 'The user TSconfig options createFoldersInEB and folderTree.hideCreateFolder were used in the past to control the existence of the "Create folder" form in Element Browser instances. With the migration of the "Create folder" view into a separate modal used in EXT:filelist, which is based on Element Browser as well, those options became useless and are therefore dropped.', + ], + ], + ], + 'markupWithSubSections' => [ + 'expected' => [ + [ + 'fragment' => 'deprecation-88839-cli-lowlevel-request-handlers', + 'snippet_title' => 'Deprecation: #88839 - CLI lowlevel request handlers', + 'snippet_content' => 'See Issue #88839' + ], + [ + 'fragment' => 'description', + 'snippet_title' => 'Description', + 'snippet_content' => implode("\n", [ + 'The interface \TYPO3\CMS\Core\Console\RequestHandlerInterface', + 'and the class \TYPO3\CMS\Core\Console\CommandRequestHandler have been introduced in TYPO3 v7 to streamline', + 'various entry points for CLI-related functionality. Back then, there were Extbase command requests and', + 'CommandLineController entry points.', + 'With TYPO3 v10, the only way to handle CLI commands is via the \TYPO3\CMS\Core\Console\CommandApplication class which is', + 'a wrapper around Symfony Console. All logic is now located in the Application, and thus, the interface and', + 'the class have been marked as deprecated.', + ]), + ], + [ + 'fragment' => 'impact', + 'snippet_title' => 'Impact', + 'snippet_content' => implode("\n", [ + 'When instantiating the CLI \TYPO3\CMS\Core\Console\RequestHandler class,', + 'a PHP E_USER_DEPRECATED error will be triggered.', + ]), + ], + [ + 'fragment' => 'affected-installations', + 'snippet_title' => 'Affected Installations', + 'snippet_content' => implode("\n", [ + 'Any TYPO3 installation having custom CLI request handlers wrapped via the interface or extending the', + 'CLI request handler class.', + ]), + ], + [ + 'fragment' => 'migration', + 'snippet_title' => 'Migration', + 'snippet_content' => 'Switch to a Symfony Command or provide a custom CLI entry point.', + ], + ] + ], + 'markupWithSubSectionsSmall' => [ + 'expected' => [ + [ + 'fragment' => 'features-and-basic-concept', + 'snippet_title' => 'Features and Basic Concept', + 'snippet_content' => 'The main goal for this blog extension was to use TYPO3s core concepts and elements to provide a full-blown blog that users of TYPO3 can instantly understand and use.' + ], + [ + 'fragment' => 'pages-as-blog-entries', + 'snippet_title' => 'Pages as blog entries', + 'snippet_content' => 'Blog entries are simply pages with a special page type blog entry and can be created and edited via the well-known page module. Creating new entries is as simple as dragging a new entry into the page tree.' + ], + ] + ], + 'markupWithCodeExamples' => [ + 'expected' => [ + [ + 'fragment' => 'rendering-page-trees', + 'snippet_title' => 'Rendering Page Trees', + 'snippet_content' => "In your backend modules you might like to show information or perform +processing for a part of the page tree. There is a whole family of +libraries in the core for making trees from records, static page trees +or page trees that can be browsed (open/close nodes). +This simple example demonstrates how to produce the HTML for a static +page tree. The result looks like: A static page tree in TYPO3 Backend The tree object itself is prepared this way (taken from +EXT:examples/Classes/Controller/DefaultController.php): At the top of the code we define the starting page and get the corresponding +page record using \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord(). Next we create an instance of \TYPO3\CMS\Backend\Tree\View\PageTreeView, +which we use for generating the tree. Notice how the BE_USER object is +called to get a SQL where clause that will ensure that only pages +that are accessible for the user will be shown in the tree! As a next step we manually add the starting page to the page tree. +This is not done automatically because it is not always a desirable +behavior. Note the use of \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIconForRecord() +to fetch the right icon for the page. Finally we get the tree to prepare itself, up to a certain depth. +Internally this will - in particular - generate a HTML part containing +the tree elements and the page icon itself. The rendered page tree is stored in a data array inside of the tree +object. We need to traverse the tree data to create the tree in HTML. +This gives us the chance to organize the tree in a table for instance. +It is this part that we pass on to the view. The result is rendered with a very simple Fluid template: We do a simple loop on the tree array of pages and display the relevant +elements." + ], + ] + ], + ]; + } + + /** + * @throws Exception + */ + private function getFixtureFileContents(string $folder, string $fileName): string + { + $fixtureFile = implode(DIRECTORY_SEPARATOR, [ + __DIR__, + 'Fixtures', + 'ParseDocumentationHTMLServiceTest', + $folder, + ucfirst($fileName) . '.html', + ]); + + if (!file_exists($fixtureFile)) { + throw new Exception("Fixture file not found: " . $fixtureFile); + } + + return file_get_contents($fixtureFile); + } }