From 8e3c78678931ae9d147bf254306eb9cda0662d4a Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 25 Feb 2026 16:34:05 +0100 Subject: [PATCH 01/11] feat: add sub-section support for pages Introduce nested sub-sections (folders with index.md) and related APIs and docs. Key changes: - Core: add Page parent/subSections properties and helpers (parent, add/get sub-sections, depth, breadcrumb, all-pages-recursive). - Generator: Section generator now detects nested index.md, assigns pages to the deepest matching section, creates section pages per language, and builds parent/child relationships. - Renderer: add Twig functions/tests (subsections, parent_section, section_breadcrumb, all_pages_recursive, section_tree, subsection, has_subsections) and extend layout lookup to consider parent section templates. - Config/docs: add pages.sections option (nested flag), update Content/Templates/Configuration docs and README release note. - Tests/fixtures: add SubSectionTests and sample pages for tutorials/advanced; register test suite in phpunit.xml.dist. Sub-sections are disabled by default and require an index.md to be recognized; enable via pages.sections.nested: true. --- README.md | 14 + config/default.php | 3 + docs/2-Content.md | 76 ++++- docs/3-Templates.md | 137 +++++++++ docs/4-Configuration.md | 18 +- phpunit.xml.dist | 3 + src/Collection/Page/Page.php | 154 ++++++++++ src/Generator/Section.php | 263 ++++++++++++++++-- src/Renderer/Extension/Core.php | 99 +++++++ src/Renderer/Layout.php | 15 + tests/SubSectionTests.php | 213 ++++++++++++++ .../Blog/Tutorials/Advanced/Tutorial 3.md | 5 + .../pages/Blog/Tutorials/Advanced/index.md | 5 + .../pages/Blog/Tutorials/Tutorial 1.md | 6 + .../pages/Blog/Tutorials/Tutorial 2.md | 6 + .../website/pages/Blog/Tutorials/index.md | 5 + 16 files changed, 987 insertions(+), 35 deletions(-) create mode 100644 tests/SubSectionTests.php create mode 100644 tests/fixtures/website/pages/Blog/Tutorials/Advanced/Tutorial 3.md create mode 100644 tests/fixtures/website/pages/Blog/Tutorials/Advanced/index.md create mode 100644 tests/fixtures/website/pages/Blog/Tutorials/Tutorial 1.md create mode 100644 tests/fixtures/website/pages/Blog/Tutorials/Tutorial 2.md create mode 100644 tests/fixtures/website/pages/Blog/Tutorials/index.md diff --git a/README.md b/README.md index d5ba0550b..4c1a58a3b 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,20 @@ curl -Lo phpdoc https://phpdoc.org/phpDocumentor.phar php phpdoc ``` +## Release + +To release a new version, create a new Git tag with the version number (e.g. `1.0.0`), push it to GitHub and the release will be automatically published by GitHub Actions. + +```bash +git tag 1.0.0 +git push origin 1.0.0 +``` + +> [!TIP] +> To create a **pre-release**, add a suffix to the version number (e.g. `1.0.0-beta.1`). + +The automated workflow also will publish the release to the [website](https://cecil.app/download), update the [Homebrew formula](https://github.com/Cecilapp/homebrew-tap) and the [Scoop manifest](https://cecil.app/scoop/cecil.json). + ## Sponsors diff --git a/config/default.php b/config/default.php index ca51d81a3..36dc36c20 100644 --- a/config/default.php +++ b/config/default.php @@ -87,6 +87,9 @@ // ] //], 'frontmatter' => 'yaml', // front matter format: `yaml`, `ini`, `toml` or `json` + 'sections' => [ // sections options + //'nested' => true, // enable sub-sections (subfolders with index.md) + ], 'body' => [ 'toc' => ['h2', 'h3'], // headers used to build the table of contents 'highlight' => false, // enables code syntax highlighting diff --git a/docs/2-Content.md b/docs/2-Content.md index 032b8684d..a78edd2e9 100644 --- a/docs/2-Content.md +++ b/docs/2-Content.md @@ -1,7 +1,7 @@ # Content @@ -30,19 +30,26 @@ Project files organization. ```plaintext ├─ pages -| ├─ blog <- Section -| | ├─ post-1.md <- Page in Section -| | └─ post-2.md +| ├─ blog # Section +| | ├─ index.md # Section's index (optional) +| | ├─ post-1.md # Page in Section +| | ├─ post-2.md +| | └─ tutorials # Sub-section +| | ├─ index.md # Sub-section's index (required) +| | ├─ tuto-1.md # Page in Sub-section +| | └─ advanced # Nested Sub-section +| | ├─ index.md +| | └─ tuto-2.md | ├─ projects | | └─ project-a.md -| └─ about.md <- Root page +| └─ about.md # Root page ├─ assets -| ├─ styles.scss <- Asset file +| ├─ styles.scss # Asset file | └─ logo.png ├─ static -| └─ file.pdf <- Static file +| └─ file.pdf # Static file └─ data - └─ authors.yml <- Data collection + └─ authors.yml # Data collection ``` ### Built website tree @@ -52,11 +59,17 @@ Result of the build. ```plaintext └─ _site - ├─ index.html <- Generated home page + ├─ index.html # Generated home page ├─ blog/ - | ├─ index.html <- Generated list of posts - | ├─ post-1/index.html <- A blog post - | └─ post-2/index.html + | ├─ index.html # Generated list of posts + | ├─ post-1/index.html # A blog post + | ├─ post-2/index.html + | └─ tutorials/ + | ├─ index.html # Sub-section list + | ├─ tuto-1/index.html + | └─ advanced/ + | ├─ index.html # Nested sub-section list + | └─ tuto-2/index.html ├─ projects/ | ├─ index.html | └─ project-a/index.html @@ -559,7 +572,7 @@ It must be the first thing in the file and must be a valid [YAML](https://en.wik | `title` | Title | File name without extension. | `Post 1` | | `layout` | Template | See [_Lookup rules_](3-Templates.md#lookup-rules). | `404` | | `date` | Creation date | File creation date (PHP _DateTime_ object). | `2019/04/15` | -| `section` | Section | Page's _Section_. | `blog` | +| `section` | Section | Page's _Section_ (or _Sub-section_). | `blog` | | `path` | Path | Page's _path_. | `blog/post-1` | | `slug` | Slug | Page's _slug_. | `post-1` | | `published` | Published or not | `true`. | `false` | @@ -749,7 +762,7 @@ In « 1_The first project.md »: ### Section -Some dedicated variables can be used in a custom _Section_ (i.e.: `
/index.md`). +Some dedicated variables can be used in a custom _Section_ (i.e.: `
/index.md`) or _Sub-section_ (i.e.: `
//index.md`). #### sortby @@ -824,6 +837,41 @@ circular: true --- ``` +### Sub-sections + +A _Sub-section_ is created when a subfolder inside a _Section_ (or another _Sub-section_) contains an `index.md` file. This enables a hierarchical tree of sections. + +:::important +The `index.md` file is **required** to turn a subfolder into a _Sub-section_. Without it, the subfolder's pages belong to the nearest parent section. +::: + +#### Structure example + +```plaintext +pages/ +└─ blog/ + ├─ index.md <- "blog" Section + ├─ post-1.md + └─ tutorials/ + ├─ index.md <- "blog/tutorials" Sub-section + ├─ tutorial-1.md + └─ advanced/ + ├─ index.md <- "blog/tutorials/advanced" Sub-section + └─ tutorial-2.md +``` + +In this example: + +- `blog` is the root _Section_, containing `post-1`. +- `blog/tutorials` is a _Sub-section_ of `blog`, containing `tutorial-1`. +- `blog/tutorials/advanced` is a _Sub-section_ of `blog/tutorials`, containing `tutorial-2`. + +Each sub-section has its own pages collection and supports the same variables as a section (`sortby`, `pagination`, `cascade`, `circular`). + +#### Accessing sub-sections in templates + +See the [Sub-sections template variables](3-Templates.md#sub-sections) for details on how to use sub-sections in Twig templates. + ### Home page Like another section, _Home page_ support `sortby` and `pagination` configuration. diff --git a/docs/3-Templates.md b/docs/3-Templates.md index 4c3f8e461..3da391466 100644 --- a/docs/3-Templates.md +++ b/docs/3-Templates.md @@ -196,6 +196,19 @@ All rules are detailed below, for each page type, in the priority order. 6. `list..twig` 7. `_default/list..twig` +#### Sub-section lookup + +For a sub-section (e.g. `blog/tutorials`), the layout lookup also includes the parent section layouts: + +1. `
//index..twig` +2. `
//list..twig` +3. `section/
/..twig` +4. `
/list..twig` _(parent section)_ +5. `section/
..twig` _(parent section)_ +6. `_default/section..twig` +7. `list..twig` +8. `_default/list..twig` + ### Type _vocabulary_ 1. `taxonomy/..twig` @@ -355,6 +368,8 @@ The `page` variable contains built-in variables of a page **and** those set in t | `page.filepath` | File system path. | `Blog/Post 1.md` | | `page.type` | `homepage`, `page`, `section`, `vocabulary` or `term`. | `page` | | `page.pages` | Collection of all sub pages. | _Collection_ | +| `page.subsections` | Collection of child sub-sections (sections only). | _Collection_ | +| `page.parent` | Parent section (sub-sections only). | _Page_ | | `page.translations` | Collection of translated pages. | _Collection_ | :::important @@ -794,6 +809,104 @@ _Example:_ The [_debug mode_](4-Configuration.md#debug) must be enabled. ::: +### Sub-sections + +The following functions and [tests](#tests) help work with [sub-sections](2-Content.md#sub-sections). Sub-sections must be [enabled in the configuration](4-Configuration.md#pages-sections). + +#### subsections + +Returns the collection of child sub-sections of a _section_ page. + +```twig +{{ subsections(page) }} +``` + +_Example:_ + +```twig +{% if page is has_subsections %} + {% for sub in subsections(page) %} + {{ sub.title }} + {% endfor %} +{% endif %} +``` + +#### parent_section + +Returns the parent section of a sub-section page (or `null` if the page is a root section). + +```twig +{{ parent_section(page) }} +``` + +_Example:_ + +```twig +{% if page is subsection %} + Back to {{ parent_section(page).title }} +{% endif %} +``` + +#### section_breadcrumb + +Returns an array of pages from the root section down to the given section, useful for building breadcrumb navigation. + +```twig +{{ section_breadcrumb(page) }} +``` + +_Example:_ + +```twig +{% for crumb in section_breadcrumb(page) %} + {{ crumb.title }} + {% if not loop.last %} / {% endif %} +{% endfor %} +``` + +#### all_pages_recursive + +Returns all pages of a section including pages from its sub-sections, recursively. + +```twig +{{ all_pages_recursive(page) }} +``` + +_Example:_ + +```twig +{% for p in all_pages_recursive(page) %} + {{ p.title }} +{% endfor %} +``` + +#### section_tree + +Returns the full hierarchical tree of all sections as an array of nodes. Each node contains a `page` entry (the section page) and a `children` array of sub-nodes. + +```twig +{{ section_tree() }} +``` + +_Example:_ + +```twig +{% macro render_tree(tree) %} +
    + {% for node in tree %} +
  • + {{ node.page.title }} + {% if node.children is not empty %} + {{ _self.render_tree(node.children) }} + {% endif %} +
  • + {% endfor %} +
+{% endmacro %} + +{{ _self.render_tree(section_tree()) }} +``` + ### d The `d()` function is the HTML version of [`dump()`](#dump) and use the [Symfony VarDumper Component](https://symfony.com/doc/5.4/components/var_dumper.html) behind the scenes. @@ -809,6 +922,30 @@ The `d()` function is the HTML version of [`dump()`](#dump) and use the [Symfony The [_debug mode_](4-Configuration.md#debug) must be enabled. ::: +## Tests + +> [Tests](https://twig.symfony.com/doc/tests/index.html) can be used with the `is` operator to check if a variable meets certain criteria. + +### subsection + +Tests if a _section_ page is a sub-section (i.e. has a parent section). + +```twig +{% if page is subsection %} + This section is a sub-section. +{% endif %} +``` + +### has_subsections + +Tests if a _section_ page has child sub-sections. + +```twig +{% if page is has_subsections %} + This section contains sub-sections. +{% endif %} +``` + ## Sorts Sorting collections (of pages, menus or taxonomies). diff --git a/docs/4-Configuration.md b/docs/4-Configuration.md index d55a09b90..b50c22bfe 100644 --- a/docs/4-Configuration.md +++ b/docs/4-Configuration.md @@ -1,7 +1,7 @@ # Configuration @@ -477,6 +477,22 @@ pages: pagination: false ``` +### pages.sections + +Options for [_Sections_](2-Content.md#section). + +#### Enable sub-sections + +Sub-sections (nested sections created by subfolders containing an `index.md` file) are disabled by default. Enable them with the `nested` option: + +```yaml +pages: + sections: + nested: true +``` + +See the [Content documentation](2-Content.md#sub-sections) for details on how to structure sub-sections and the [Templates documentation](3-Templates.md#sub-sections) for available functions and tests. + ### pages.paths Apply a custom [`path`](2-Content.md#predefined-variables) for all pages of a **_Section_**. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 05652e425..e616f3a48 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -27,6 +27,9 @@ ./tests/IntegrationCliTests.php + + ./tests/SubSectionTests.php + diff --git a/src/Collection/Page/Page.php b/src/Collection/Page/Page.php index ed7a60378..14c3c98fd 100644 --- a/src/Collection/Page/Page.php +++ b/src/Collection/Page/Page.php @@ -71,6 +71,12 @@ class Page extends Item /** @var array */ protected $paginator = []; + /** @var Page|null Parent section (for sub-sections). */ + protected $parentSection; + + /** @var Collection|null Child sub-sections collection. */ + protected $subSections; + /** @var \Cecil\Collection\Taxonomy\Vocabulary Terms of a vocabulary. */ protected $terms; @@ -517,6 +523,154 @@ public function getPagination(): array return $this->getPaginator(); } + /** + * Set the parent section (for sub-sections). + */ + public function setParentSection(?Page $parent): self + { + $this->parentSection = $parent; + + return $this; + } + + /** + * Get the parent section (for sub-sections). + */ + public function getParentSection(): ?Page + { + return $this->parentSection; + } + + /** + * Does this section have a parent section? + */ + public function hasParentSection(): bool + { + return $this->parentSection !== null; + } + + /** + * Get child sub-sections collection. + */ + public function getSubSections(): ?Collection + { + return $this->subSections; + } + + /** + * Set child sub-sections collection. + */ + public function setSubSections(Collection $subSections): self + { + $this->subSections = $subSections; + + return $this; + } + + /** + * Add a child sub-section. + */ + public function addSubSection(Page $child): self + { + if ($this->subSections === null) { + $this->subSections = new Collection(\sprintf('%s-subsections', $this->getId())); + } + + try { + $this->subSections->add($child); + } catch (\DomainException) { + $this->subSections->replace($child->getId(), $child); + } + + return $this; + } + + /** + * Does this section have child sub-sections? + */ + public function hasSubSections(): bool + { + return $this->subSections !== null && \count($this->subSections) > 0; + } + + /** + * Is this a sub-section (has a parent section)? + */ + public function isSubSection(): bool + { + return $this->type === Type::SECTION && $this->parentSection !== null; + } + + /** + * Get depth level in the section tree (0 = root section). + * Uses the parent section chain rather than path slashes for robustness + * when custom path patterns are configured. + */ + public function getSectionDepth(): int + { + $depth = 0; + $current = $this; + + while ($current->hasParentSection()) { + $depth++; + $current = $current->getParentSection(); + } + + return $depth; + } + + /** + * Returns the breadcrumb from root section to this section. + * + * @return Page[] + */ + public function getSectionBreadcrumb(): array + { + $breadcrumb = [$this]; + $current = $this; + + while ($current->hasParentSection()) { + $current = $current->getParentSection(); + array_unshift($breadcrumb, $current); + } + + return $breadcrumb; + } + + /** + * Get all pages recursively, including pages from sub-sections. + */ + public function getAllPagesRecursive(): Collection + { + $allPages = new Collection(\sprintf('%s-all-pages', $this->getId())); + + // Add direct pages + if ($this->getPages() !== null) { + foreach ($this->getPages() as $page) { + try { + $allPages->add($page); + } catch (\DomainException) { + // skip duplicates + } + } + } + + // Add pages from sub-sections recursively + if ($this->hasSubSections()) { + foreach ($this->getSubSections() as $subSection) { + foreach ($subSection->getAllPagesRecursive() as $page) { + try { + $allPages->add($page); + } catch (\DomainException) { + // skip duplicates + } + } + } + } + + return $allPages; + } + /** * Set vocabulary terms. */ diff --git a/src/Generator/Section.php b/src/Generator/Section.php index c3ec3b2e2..c720115fd 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -15,6 +15,7 @@ use Cecil\Collection\Page\Collection as PagesCollection; use Cecil\Collection\Page\Page; +use Cecil\Collection\Page\PrefixSuffix; use Cecil\Collection\Page\Type; use Cecil\Exception\RuntimeException; @@ -26,6 +27,12 @@ * creates a new page for each section. The generated pages are added to the * collection of generated pages. It also handles sorting of subpages and * adding navigation links (next and previous) to the section pages. + * + * Sub-sections support: + * When a subfolder inside pages/ contains an index.md file, it is treated as a + * sub-section. Pages within that subfolder are assigned to the sub-section rather + * than the root section. Parent/child relationships are established between + * sections to form a tree structure. */ class Section extends AbstractGenerator implements GeneratorInterface { @@ -34,35 +41,113 @@ class Section extends AbstractGenerator implements GeneratorInterface */ public function generate(): void { + // Step 1: Detect nested section paths (subfolders with index.md). + // Returns a map of slugified-folder-path => page-id. + $nestedSectionPaths = $this->detectNestedSectionPaths(); + + // Build a reverse map: page-id => folder-path (for looking up a page's original folder). + $pageIdToFolderPath = []; + foreach ($this->builder->getPages() ?? [] as $p) { + $filepath = $p->getVariable('filepath'); + if ($filepath) { + $dir = str_replace(DIRECTORY_SEPARATOR, '/', \dirname($filepath)); + $folderPath = ($dir === '.') ? '' : Page::slugify($dir); + $pageIdToFolderPath[$p->getId()] = $folderPath; + } + } + + // Step 2: Group pages into sections (deepest matching section). $sections = []; - // identifying sections from all pages /** @var Page $page */ foreach ($this->builder->getPages() ?? [] as $page) { - // top level (root) sections - if ($page->getSection()) { - // do not add "not published" and "not excluded" pages to its section - if ( - $page->getVariable('published') !== true - || ($page->getVariable('excluded') || $page->getVariable('exclude')) - ) { - continue; + if (!$page->getSection()) { + continue; + } + // do not add "not published" and "not excluded" pages to its section + if ( + $page->getVariable('published') !== true + || ($page->getVariable('excluded') || $page->getVariable('exclude')) + ) { + continue; + } + + $language = $page->getVariable('language', $this->config->getLanguageDefault()); + $pageId = $page->getId(); + + // Use the original file folder path to resolve the section. + $originalFolder = $pageIdToFolderPath[$pageId] ?? null; + $sectionPath = $this->resolveSection($originalFolder, $page->getSection(), $nestedSectionPaths); + + // Don't add a section's own index page to its pages list. + // A page is a section index if its page ID matches a nested section path. + if ($pageId === $sectionPath || isset($nestedSectionPaths[$pageId])) { + continue; + } + + // Root section index pages: their path equals their section. + $pagePath = $page->getPath(); + if ($pagePath === $page->getSection()) { + continue; + } + + // Update the page's section to the resolved (possibly nested) section path. + if ($sectionPath !== $page->getSection()) { + $page->setSection($sectionPath); + } + + $sections[$sectionPath][$language][] = $page; + } + + // Ensure all nested section paths exist in the sections map (even if empty). + // Also ensure their parent sections exist. + foreach ($nestedSectionPaths as $nestedPath => $_) { + if (!isset($sections[$nestedPath])) { + // Determine language from the index page + if ($this->builder->getPages()->has($nestedPath)) { + $indexPage = $this->builder->getPages()->get($nestedPath); + $lang = $indexPage->getVariable('language', $this->config->getLanguageDefault()); + $sections[$nestedPath][$lang] = []; } - $sections[$page->getSection()][$page->getVariable('language', $this->config->getLanguageDefault())][] = $page; + } + + // Walk up parent paths and ensure they exist as sections too. + $parts = explode('/', $nestedPath); + array_pop($parts); + while (!empty($parts)) { + $parentPath = implode('/', $parts); + if (!isset($sections[$parentPath])) { + $parentSlug = Page::slugify($parentPath); + if ($this->builder->getPages()->has($parentSlug)) { + $indexPage = $this->builder->getPages()->get($parentSlug); + $lang = $indexPage->getVariable('language', $this->config->getLanguageDefault()); + $sections[$parentPath][$lang] = []; + } + } + array_pop($parts); } } - // adds each section to pages collection + // Step 3: Create section pages. if (\count($sections) > 0) { $menuWeight = 100; - foreach ($sections as $section => $languages) { + // Sort section keys so parents are processed before children. + $sectionKeys = array_keys($sections); + usort($sectionKeys, function (string $a, string $b): int { + return substr_count($a, '/') <=> substr_count($b, '/'); + }); + + $sectionPages = []; // maps sectionPath/language => Page + + foreach ($sectionKeys as $section) { + $languages = $sections[$section]; foreach ($languages as $language => $pagesAsArray) { $pageId = $path = Page::slugify($section); if ($language != $this->config->getLanguageDefault()) { $pageId = "$language/$pageId"; } - $page = (new Page($pageId))->setVariable('title', ucfirst($section)) + $page = (new Page($pageId))->setVariable('title', ucfirst(basename($section))) ->setPath($path); if ($this->builder->getPages()->has($pageId)) { $page = clone $this->builder->getPages()->get($pageId); @@ -85,23 +170,25 @@ public function generate(): void $sortBy = $page->getVariable('sortby') ?? $this->config->get('pages.sortby'); $pages = $pages->sortBy($sortBy); // adds navigation links (excludes taxonomy pages) - $sortBy = $page->getVariable('sortby')['variable'] ?? $page->getVariable('sortby') ?? $this->config->get('pages.sortby')['variable'] ?? $this->config->get('pages.sortby') ?? 'date'; + $sortByVar = $page->getVariable('sortby')['variable'] ?? $page->getVariable('sortby') ?? $this->config->get('pages.sortby')['variable'] ?? $this->config->get('pages.sortby') ?? 'date'; if (!\in_array($page->getId(), array_keys((array) $this->config->get('taxonomies')))) { - $this->addNavigationLinks($pages, $sortBy, $page->getVariable('circular') ?? false); + $this->addNavigationLinks($pages, $sortByVar, $page->getVariable('circular') ?? false); } // creates page for each section $page->setType(Type::SECTION->value) ->setSection($path) ->setPages($pages) ->setVariable('language', $language) - ->setVariable('date', $pages->first()->getVariable('date')) ->setVariable('langref', $path); + if ($pages->first()) { + $page->setVariable('date', $pages->first()->getVariable('date')); + } // human readable title if ($page->getVariable('title') == 'index') { - $page->setVariable('title', $section); + $page->setVariable('title', basename($section)); } - // default menu - if (!$page->getVariable('menu')) { + // default menu: only root sections get a default menu entry + if (!str_contains($section, '/') && !$page->getVariable('menu')) { $page->setVariable('menu', ['main' => ['weight' => $menuWeight]]); } @@ -110,8 +197,144 @@ public function generate(): void } catch (\DomainException) { $this->generatedPages->replace($page->getId(), $page); } + + $sectionPages["$path|$language"] = $page; } - $menuWeight += 10; + + if (!str_contains($section, '/')) { + $menuWeight += 10; + } + } + + // Step 4: Build parent/child relationships between sections. + $this->buildSectionTree($sectionPages, $nestedSectionPaths); + } + } + + /** + * Detects nested section paths by finding pages created from index.md files + * that are in subdirectories (nested deeper than the root section level). + * + * Uses original file paths (not transformed page paths) to correctly detect + * hierarchy even when custom path patterns (e.g., date-based paths) are configured. + * + * @return array Map of nested section paths (slugified folder paths) + */ + protected function detectNestedSectionPaths(): array + { + $nestedPaths = []; + + /** @var Page $page */ + foreach ($this->builder->getPages() ?? [] as $page) { + if ($page->isVirtual() || $page->getType() === Type::HOMEPAGE->value) { + continue; + } + + $filepath = $page->getVariable('filepath'); + if (!$filepath) { + continue; + } + + // Get the original directory from the filepath. + $dir = str_replace(DIRECTORY_SEPARATOR, '/', \dirname($filepath)); + if ($dir === '.' || !str_contains($dir, '/')) { + continue; // Root-level folders are not "nested" + } + + // Check if this page was created from an index file. + $extension = pathinfo($filepath, PATHINFO_EXTENSION); + $filename = basename($filepath, '.' . $extension); + $cleanName = strtolower(PrefixSuffix::sub($filename)); + + if ($cleanName === 'index' || $cleanName === 'readme') { + // Use the slugified directory as the nested section path. + $folderPath = Page::slugify($dir); + $nestedPaths[$folderPath] = true; + } + } + + return $nestedPaths; + } + + /** + * Resolves the deepest matching section for a page based on its original folder path. + * + * If the page's original folder matches a nested section path, it is assigned to + * that sub-section. Otherwise, it stays in its root section. + * + * @param string|null $originalFolder The page's original file folder (slugified) + * @param string $rootSection The page's current root section + * @param array $nestedSectionPaths Map of nested section paths + * + * @return string The resolved section path + */ + protected function resolveSection(?string $originalFolder, string $rootSection, array $nestedSectionPaths): string + { + if ($originalFolder === null || empty($nestedSectionPaths)) { + return $rootSection; + } + + // Try to find the deepest nested section matching this page's original folder. + // Start from the full folder path and walk up. + $parts = explode('/', $originalFolder); + + while (!empty($parts)) { + $candidate = implode('/', $parts); + if (isset($nestedSectionPaths[$candidate])) { + return $candidate; + } + array_pop($parts); + } + + return $rootSection; + } + + /** + * Builds parent/child relationships between section pages. + * + * @param array $sectionPages Map of "path|language" => section Page + * @param array $nestedSectionPaths Map of nested section paths + */ + protected function buildSectionTree(array $sectionPages, array $nestedSectionPaths): void + { + foreach ($sectionPages as $key => $sectionPage) { + [$path, $language] = explode('|', $key); + + if (!str_contains($path, '/')) { + continue; // Root sections have no parent + } + + // Find the closest parent section. + $parts = explode('/', $path); + array_pop($parts); + + while (!empty($parts)) { + $parentPath = implode('/', $parts); + $parentKey = "$parentPath|$language"; + + if (isset($sectionPages[$parentKey])) { + $parentPage = $sectionPages[$parentKey]; + + // Set parent/child relationship + $sectionPage->setParentSection($parentPage); + $parentPage->addSubSection($sectionPage); + + // Update generated pages collections + try { + $this->generatedPages->replace($sectionPage->getId(), $sectionPage); + } catch (\DomainException) { + // ignore + } + try { + $this->generatedPages->replace($parentPage->getId(), $parentPage); + } catch (\DomainException) { + // ignore + } + + break; + } + + array_pop($parts); } } } diff --git a/src/Renderer/Extension/Core.php b/src/Renderer/Extension/Core.php index 7e35af031..ba154dd3d 100644 --- a/src/Renderer/Extension/Core.php +++ b/src/Renderer/Extension/Core.php @@ -98,6 +98,12 @@ public function getFunctions() // content new \Twig\TwigFunction('readtime', [$this, 'readtime']), new \Twig\TwigFunction('hash', [$this, 'hash']), + // sub-sections + new \Twig\TwigFunction('subsections', [$this, 'getSubSections']), + new \Twig\TwigFunction('parent_section', [$this, 'getParentSectionFunc']), + new \Twig\TwigFunction('section_breadcrumb', [$this, 'getSectionBreadcrumb']), + new \Twig\TwigFunction('all_pages_recursive', [$this, 'getAllPagesRecursive']), + new \Twig\TwigFunction('section_tree', [$this, 'getSectionTree']), // others new \Twig\TwigFunction('getenv', [$this, 'getEnv']), new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]), @@ -194,6 +200,13 @@ public function getTests() new \Twig\TwigTest('asset', [$this, 'isAsset']), new \Twig\TwigTest('image_large', [$this, 'isImageLarge']), new \Twig\TwigTest('image_square', [$this, 'isImageSquare']), + // sub-sections + new \Twig\TwigTest('subsection', function (Page $page): bool { + return $page->isSubSection(); + }), + new \Twig\TwigTest('has_subsections', function (Page $page): bool { + return $page->hasSubSections(); + }), ]; } @@ -205,6 +218,92 @@ public function filterBySection(PagesCollection $pages, string $section): Collec return $this->filterBy($pages, 'section', $section); } + /** + * Returns child sub-sections of a section page. + * + * @return Page[]|PagesCollection|null + */ + public function getSubSections(Page $page): ?\Cecil\Collection\Page\Collection + { + if ($page->getType() !== Type::SECTION->value) { + return null; + } + + return $page->getSubSections(); + } + + /** + * Returns parent section of a sub-section. + */ + public function getParentSectionFunc(Page $page): ?Page + { + return $page->getParentSection(); + } + + /** + * Returns breadcrumb from root section to the given section. + * + * @return Page[] + */ + public function getSectionBreadcrumb(Page $page): array + { + return $page->getSectionBreadcrumb(); + } + + /** + * Returns all pages recursively, including pages from sub-sections. + */ + public function getAllPagesRecursive(Page $page): ?\Cecil\Collection\Page\Collection + { + if ($page->getType() !== Type::SECTION->value) { + return null; + } + + return $page->getAllPagesRecursive(); + } + + /** + * Returns the full section tree (root sections with nested children). + * + * @return array + */ + public function getSectionTree(): array + { + $tree = []; + $pages = $this->builder->getPages(); + if ($pages === null) { + return $tree; + } + + /** @var Page $page */ + foreach ($pages as $page) { + if ($page->getType() === Type::SECTION->value && !$page->hasParentSection()) { + $tree[$page->getId()] = $this->buildTreeNode($page); + } + } + + return $tree; + } + + /** + * Recursively builds a tree node for section_tree(). + */ + private function buildTreeNode(Page $page): array + { + $node = [ + 'page' => $page, + 'children' => [], + ]; + + if ($page->hasSubSections()) { + foreach ($page->getSubSections() as $child) { + $node['children'][$child->getId()] = $this->buildTreeNode($child); + } + } + + return $node; + } + /** * Filters a pages collection by variable's name/value. */ diff --git a/src/Renderer/Layout.php b/src/Renderer/Layout.php index c4100caa0..3cb2cc42f 100644 --- a/src/Renderer/Layout.php +++ b/src/Renderer/Layout.php @@ -127,6 +127,21 @@ protected static function lookup(CollectionPage $page, string $format, \Cecil\Co $layouts = array_merge(["section/{$section}.$format.$ext"], $layouts); $layouts = array_merge(["{$section}/list.$format.$ext"], $layouts); $layouts = array_merge(["{$section}/index.$format.$ext"], $layouts); + // Sub-section support: also try parent section layouts. + // For "blog/tutorials", also try "blog/list", "blog/index", "section/blog". + if (str_contains($section, '/')) { + $parentSection = substr($section, 0, strrpos($section, '/')); + $layouts = array_merge( + [ + "{$section}/index.$format.$ext", + "{$section}/list.$format.$ext", + "section/{$section}.$format.$ext", + "{$parentSection}/list.$format.$ext", + "section/{$parentSection}.$format.$ext", + ], + $layouts + ); + } } if ($page->hasVariable('layout')) { $layouts = array_merge(["$layout.$format.$ext"], $layouts); diff --git a/tests/SubSectionTests.php b/tests/SubSectionTests.php new file mode 100644 index 000000000..d64a26268 --- /dev/null +++ b/tests/SubSectionTests.php @@ -0,0 +1,213 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Cecil\Test; + +use Cecil\Builder; +use Cecil\Collection\Page\Page; +use Cecil\Collection\Page\Type; +use Cecil\Config; +use Cecil\Logger\PrintLogger; +use Cecil\Util; +use Symfony\Component\Filesystem\Filesystem; + +class SubSectionTests extends \PHPUnit\Framework\TestCase +{ + protected static $source; + protected static $config; + protected static $destination; + protected static $builder; + + public static function setUpBeforeClass(): void + { + self::$source = Util::joinFile(__DIR__, 'fixtures/website'); + self::$config = Util::joinFile(self::$source, 'config.yml'); + self::$destination = self::$source; + + putenv('CECIL_DEBUG=true'); + self::$builder = Builder::create(Config::loadFile(self::$config), new PrintLogger(Builder::VERBOSITY_DEBUG)) + ->setSourceDir(self::$source) + ->setDestinationDir(self::$destination); + self::$builder->build([ + 'drafts' => true, + 'dry-run' => true, + ]); + } + + public static function tearDownAfterClass(): void + { + $fs = new Filesystem(); + $fs->remove(Util::joinFile(self::$destination, '.cecil')); + $fs->remove(Util::joinFile(self::$destination, '.cache')); + $fs->remove(Util::joinFile(self::$destination, '_site')); + } + + protected function getBuilder(): Builder + { + return self::$builder; + } + + /** + * Test that sub-section pages are created. + */ + public function testSubSectionPagesExist() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + // The "blog/tutorials" sub-section should exist as a Section-type page. + $this->assertTrue($pages->has('blog/tutorials'), 'Sub-section "blog/tutorials" should exist'); + $tutorialsPage = $pages->get('blog/tutorials'); + $this->assertEquals(Type::SECTION->value, $tutorialsPage->getType(), '"blog/tutorials" should be a section'); + + // The "blog/tutorials/advanced" sub-section should also exist. + $this->assertTrue($pages->has('blog/tutorials/advanced'), 'Sub-section "blog/tutorials/advanced" should exist'); + $advancedPage = $pages->get('blog/tutorials/advanced'); + $this->assertEquals(Type::SECTION->value, $advancedPage->getType(), '"blog/tutorials/advanced" should be a section'); + } + + /** + * Test parent/child relationships. + */ + public function testParentChildRelationships() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + $blogPage = $pages->get('blog'); + $tutorialsPage = $pages->get('blog/tutorials'); + $advancedPage = $pages->get('blog/tutorials/advanced'); + + // blog should have sub-sections + $this->assertTrue($blogPage->hasSubSections(), '"blog" should have sub-sections'); + + // tutorials should be a sub-section of blog + $this->assertTrue($tutorialsPage->isSubSection(), '"blog/tutorials" should be a sub-section'); + $this->assertTrue($tutorialsPage->hasParentSection(), '"blog/tutorials" should have a parent section'); + $this->assertEquals('blog', $tutorialsPage->getParentSection()->getId(), 'Parent of "blog/tutorials" should be "blog"'); + + // advanced should be a sub-section of tutorials + $this->assertTrue($advancedPage->isSubSection(), '"blog/tutorials/advanced" should be a sub-section'); + $this->assertEquals('blog/tutorials', $advancedPage->getParentSection()->getId()); + + // blog should NOT be a sub-section + $this->assertFalse($blogPage->isSubSection(), '"blog" should NOT be a sub-section'); + } + + /** + * Test that pages are assigned to the correct (deepest) section. + */ + public function testPagesAssignedToDeepestSection() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + $tutorialsPage = $pages->get('blog/tutorials'); + $advancedPage = $pages->get('blog/tutorials/advanced'); + + // Tutorials section should contain its direct pages + $tutorialPages = $tutorialsPage->getPages(); + $this->assertNotNull($tutorialPages, 'Tutorials section should have pages'); + $this->assertGreaterThanOrEqual(2, \count($tutorialPages), 'Tutorials section should have at least 2 pages'); + + // Advanced section should contain its direct pages + $advancedPages = $advancedPage->getPages(); + $this->assertNotNull($advancedPages, 'Advanced section should have pages'); + $this->assertGreaterThanOrEqual(1, \count($advancedPages), 'Advanced section should have at least 1 page'); + } + + /** + * Test section depth calculation. + */ + public function testSectionDepth() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + $blogPage = $pages->get('blog'); + $tutorialsPage = $pages->get('blog/tutorials'); + $advancedPage = $pages->get('blog/tutorials/advanced'); + + $this->assertEquals(0, $blogPage->getSectionDepth(), '"blog" depth should be 0'); + $this->assertEquals(1, $tutorialsPage->getSectionDepth(), '"blog/tutorials" depth should be 1'); + $this->assertEquals(2, $advancedPage->getSectionDepth(), '"blog/tutorials/advanced" depth should be 2'); + } + + /** + * Test section breadcrumb. + */ + public function testSectionBreadcrumb() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + $advancedPage = $pages->get('blog/tutorials/advanced'); + $breadcrumb = $advancedPage->getSectionBreadcrumb(); + + $this->assertCount(3, $breadcrumb, 'Breadcrumb for "blog/tutorials/advanced" should have 3 items'); + $this->assertEquals('blog', $breadcrumb[0]->getId()); + $this->assertEquals('blog/tutorials', $breadcrumb[1]->getId()); + $this->assertEquals('blog/tutorials/advanced', $breadcrumb[2]->getId()); + } + + /** + * Test getAllPagesRecursive. + */ + public function testGetAllPagesRecursive() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + $tutorialsPage = $pages->get('blog/tutorials'); + $allPages = $tutorialsPage->getAllPagesRecursive(); + + // Should include direct pages (2 tutorials) + advanced pages (1 tutorial) + $this->assertGreaterThanOrEqual(3, \count($allPages), 'Recursive pages should include sub-section pages'); + } + + /** + * Test that root section no longer contains sub-section pages. + */ + public function testRootSectionExcludesSubSectionPages() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + $blogPage = $pages->get('blog'); + $blogDirectPages = $blogPage->getPages(); + + // Blog section's direct pages should NOT include tutorial pages + if ($blogDirectPages !== null) { + foreach ($blogDirectPages as $page) { + // Check page section is "blog", not a sub-section + $this->assertEquals( + 'blog', + $page->getSection(), + \sprintf('Blog direct page "%s" should have section "blog"', $page->getId()) + ); + } + } + } + + /** + * Test that non-sub-section folders without index.md are not treated as sub-sections. + * (backward compatibility) + */ + public function testFoldersWithoutIndexAreNotSubSections() + { + $builder = $this->getBuilder(); + $pages = $builder->getPages(); + + // The "assets" folder under Blog should NOT create a sub-section + // (there's no blog/assets/index.md) + $this->assertFalse($pages->has('blog/assets'), 'Folders without index.md should NOT become sub-sections'); + } +} diff --git a/tests/fixtures/website/pages/Blog/Tutorials/Advanced/Tutorial 3.md b/tests/fixtures/website/pages/Blog/Tutorials/Advanced/Tutorial 3.md new file mode 100644 index 000000000..db7ae2532 --- /dev/null +++ b/tests/fixtures/website/pages/Blog/Tutorials/Advanced/Tutorial 3.md @@ -0,0 +1,5 @@ +--- +title: Advanced Generics Tutorial +date: 2025-03-10 +--- +An advanced tutorial about generics. diff --git a/tests/fixtures/website/pages/Blog/Tutorials/Advanced/index.md b/tests/fixtures/website/pages/Blog/Tutorials/Advanced/index.md new file mode 100644 index 000000000..5b8ebdc99 --- /dev/null +++ b/tests/fixtures/website/pages/Blog/Tutorials/Advanced/index.md @@ -0,0 +1,5 @@ +--- +title: Advanced Tutorials +date: 2025-03-01 +--- +Section of advanced tutorials. diff --git a/tests/fixtures/website/pages/Blog/Tutorials/Tutorial 1.md b/tests/fixtures/website/pages/Blog/Tutorials/Tutorial 1.md new file mode 100644 index 000000000..4a89ac15c --- /dev/null +++ b/tests/fixtures/website/pages/Blog/Tutorials/Tutorial 1.md @@ -0,0 +1,6 @@ +--- +title: PHP Tutorial +date: 2025-02-01 +tags: [Tag 1] +--- +A tutorial about PHP. diff --git a/tests/fixtures/website/pages/Blog/Tutorials/Tutorial 2.md b/tests/fixtures/website/pages/Blog/Tutorials/Tutorial 2.md new file mode 100644 index 000000000..5749ce7d7 --- /dev/null +++ b/tests/fixtures/website/pages/Blog/Tutorials/Tutorial 2.md @@ -0,0 +1,6 @@ +--- +title: JavaScript Tutorial +date: 2025-02-15 +tags: [Tag 2] +--- +A tutorial about JavaScript. diff --git a/tests/fixtures/website/pages/Blog/Tutorials/index.md b/tests/fixtures/website/pages/Blog/Tutorials/index.md new file mode 100644 index 000000000..2bdf5607e --- /dev/null +++ b/tests/fixtures/website/pages/Blog/Tutorials/index.md @@ -0,0 +1,5 @@ +--- +title: Tutorials +date: 2025-01-15 +--- +Section of tutorials. From 28702daa349bf9d1017cfdd3b523a6e320b03cc5 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Thu, 26 Feb 2026 14:37:43 +0100 Subject: [PATCH 02/11] chore: update deps --- composer.lock | 249 +++++++++++++++++++++++++------------------------- 1 file changed, 125 insertions(+), 124 deletions(-) diff --git a/composer.lock b/composer.lock index d6e2c220c..576b37187 100644 --- a/composer.lock +++ b/composer.lock @@ -1806,16 +1806,16 @@ }, { "name": "symfony/cache", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "8dde98d5a4123b53877aca493f9be57b333f14bd" + "reference": "1d06192e8f164e2729b0031e6807d72a6195b8bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/8dde98d5a4123b53877aca493f9be57b333f14bd", - "reference": "8dde98d5a4123b53877aca493f9be57b333f14bd", + "url": "https://api.github.com/repos/symfony/cache/zipball/1d06192e8f164e2729b0031e6807d72a6195b8bb", + "reference": "1d06192e8f164e2729b0031e6807d72a6195b8bb", "shasum": "" }, "require": { @@ -1886,7 +1886,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.5" + "source": "https://github.com/symfony/cache/tree/v7.4.6" }, "funding": [ { @@ -1906,7 +1906,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:16:02+00:00" + "time": "2026-02-21T23:29:27+00:00" }, { "name": "symfony/cache-contracts", @@ -1986,16 +1986,16 @@ }, { "name": "symfony/config", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb" + "reference": "9400e2f9226b3b64ebb0a8ae967ae84e54e39640" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4275b53b8ab0cf37f48bf273dc2285c8178efdfb", - "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb", + "url": "https://api.github.com/repos/symfony/config/zipball/9400e2f9226b3b64ebb0a8ae967ae84e54e39640", + "reference": "9400e2f9226b3b64ebb0a8ae967ae84e54e39640", "shasum": "" }, "require": { @@ -2041,7 +2041,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.4" + "source": "https://github.com/symfony/config/tree/v7.4.6" }, "funding": [ { @@ -2061,20 +2061,20 @@ "type": "tidelift" } ], - "time": "2026-01-13T11:36:38+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/console", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" + "reference": "6d643a93b47398599124022eb24d97c153c12f27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", + "reference": "6d643a93b47398599124022eb24d97c153c12f27", "shasum": "" }, "require": { @@ -2139,7 +2139,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.4" + "source": "https://github.com/symfony/console/tree/v7.4.6" }, "funding": [ { @@ -2159,20 +2159,20 @@ "type": "tidelift" } ], - "time": "2026-01-13T11:36:38+00:00" + "time": "2026-02-25T17:02:47+00:00" }, { "name": "symfony/css-selector", - "version": "v7.4.0", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135" + "reference": "2e7c52c647b406e2107dd867db424a4dbac91864" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/ab862f478513e7ca2fe9ec117a6f01a8da6e1135", - "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2e7c52c647b406e2107dd867db424a4dbac91864", + "reference": "2e7c52c647b406e2107dd867db424a4dbac91864", "shasum": "" }, "require": { @@ -2208,7 +2208,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.0" + "source": "https://github.com/symfony/css-selector/tree/v7.4.6" }, "funding": [ { @@ -2228,7 +2228,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T13:39:42+00:00" + "time": "2026-02-17T07:53:42+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2299,16 +2299,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "71fd6a82fc357c8b5de22f78b228acfc43dee965" + "reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/71fd6a82fc357c8b5de22f78b228acfc43dee965", - "reference": "71fd6a82fc357c8b5de22f78b228acfc43dee965", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/487ba8fa43da9a8e6503fe939b45ecd96875410e", + "reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e", "shasum": "" }, "require": { @@ -2347,7 +2347,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.4.4" + "source": "https://github.com/symfony/dom-crawler/tree/v7.4.6" }, "funding": [ { @@ -2367,20 +2367,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T08:47:25+00:00" + "time": "2026-02-17T07:53:42+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", "shasum": "" }, "require": { @@ -2417,7 +2417,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v7.4.6" }, "funding": [ { @@ -2437,20 +2437,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/finder", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", - "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", "shasum": "" }, "require": { @@ -2485,7 +2485,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.5" + "source": "https://github.com/symfony/finder/tree/v7.4.6" }, "funding": [ { @@ -2505,20 +2505,20 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-01-29T09:40:50+00:00" }, { "name": "symfony/intl", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "7fa2d46174166bcd7829abc8717949f8a0b21fb7" + "reference": "6d6a398b18f73b3110140dbb030dcee2ae4ea81f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/7fa2d46174166bcd7829abc8717949f8a0b21fb7", - "reference": "7fa2d46174166bcd7829abc8717949f8a0b21fb7", + "url": "https://api.github.com/repos/symfony/intl/zipball/6d6a398b18f73b3110140dbb030dcee2ae4ea81f", + "reference": "6d6a398b18f73b3110140dbb030dcee2ae4ea81f", "shasum": "" }, "require": { @@ -2575,7 +2575,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v7.4.4" + "source": "https://github.com/symfony/intl/tree/v7.4.6" }, "funding": [ { @@ -2595,20 +2595,20 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:19:02+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "symfony/mime", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148" + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148", - "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148", + "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", "shasum": "" }, "require": { @@ -2619,7 +2619,7 @@ }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/reflection-docblock": "<5.2|>=7", "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" @@ -2627,7 +2627,7 @@ "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^5.2", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/process": "^6.4|^7.0|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0", @@ -2664,7 +2664,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.5" + "source": "https://github.com/symfony/mime/tree/v7.4.6" }, "funding": [ { @@ -2684,7 +2684,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T08:59:58+00:00" + "time": "2026-02-05T15:57:06+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3504,16 +3504,16 @@ }, { "name": "symfony/property-info", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21" + "reference": "6396b28f44d7c28b209a1bd73acf0dd985a0a4ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/1c9d326bd69602561e2ea467a16c09b5972eee21", - "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21", + "url": "https://api.github.com/repos/symfony/property-info/zipball/6396b28f44d7c28b209a1bd73acf0dd985a0a4ef", + "reference": "6396b28f44d7c28b209a1bd73acf0dd985a0a4ef", "shasum": "" }, "require": { @@ -3523,14 +3523,14 @@ "symfony/type-info": "~7.3.10|^7.4.4|^8.0.4" }, "conflict": { - "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/reflection-docblock": "<5.2|>=7", "phpdocumentor/type-resolver": "<1.5.1", "symfony/cache": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/serializer": "<6.4" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^5.2", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "phpstan/phpdoc-parser": "^1.0|^2.0", "symfony/cache": "^6.4|^7.0|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0", @@ -3570,7 +3570,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.4.5" + "source": "https://github.com/symfony/property-info/tree/v7.4.6" }, "funding": [ { @@ -3590,20 +3590,20 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:16:02+00:00" + "time": "2026-02-13T11:51:31+00:00" }, { "name": "symfony/serializer", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47" + "reference": "83c3cbd6dcb96c1dbe197499a0714f8dceb0f274" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/480cd1237c98ab1219c20945b92c9d4480a44f47", - "reference": "480cd1237c98ab1219c20945b92c9d4480a44f47", + "url": "https://api.github.com/repos/symfony/serializer/zipball/83c3cbd6dcb96c1dbe197499a0714f8dceb0f274", + "reference": "83c3cbd6dcb96c1dbe197499a0714f8dceb0f274", "shasum": "" }, "require": { @@ -3613,17 +3613,18 @@ "symfony/polyfill-php84": "^1.30" }, "conflict": { - "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/reflection-docblock": "<5.2|>=7", "phpdocumentor/type-resolver": "<1.5.1", "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", + "symfony/type-info": "<7.2.5", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^5.2", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "phpstan/phpdoc-parser": "^1.0|^2.0", "seld/jsonlint": "^1.10", "symfony/cache": "^6.4|^7.0|^8.0", @@ -3640,7 +3641,7 @@ "symfony/property-access": "^6.4|^7.0|^8.0", "symfony/property-info": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.1.8|^8.0", + "symfony/type-info": "^7.2.5|^8.0", "symfony/uid": "^6.4|^7.0|^8.0", "symfony/validator": "^6.4|^7.0|^8.0", "symfony/var-dumper": "^6.4|^7.0|^8.0", @@ -3673,7 +3674,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.4.5" + "source": "https://github.com/symfony/serializer/tree/v7.4.6" }, "funding": [ { @@ -3693,7 +3694,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T08:59:58+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/service-contracts", @@ -3784,16 +3785,16 @@ }, { "name": "symfony/string", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f" + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f", - "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", "shasum": "" }, "require": { @@ -3851,7 +3852,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.4" + "source": "https://github.com/symfony/string/tree/v7.4.6" }, "funding": [ { @@ -3871,20 +3872,20 @@ "type": "tidelift" } ], - "time": "2026-01-12T10:54:30+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "symfony/translation", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "bfde13711f53f549e73b06d27b35a55207528877" + "reference": "1888cf064399868af3784b9e043240f1d89d25ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/bfde13711f53f549e73b06d27b35a55207528877", - "reference": "bfde13711f53f549e73b06d27b35a55207528877", + "url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce", + "reference": "1888cf064399868af3784b9e043240f1d89d25ce", "shasum": "" }, "require": { @@ -3951,7 +3952,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.4" + "source": "https://github.com/symfony/translation/tree/v7.4.6" }, "funding": [ { @@ -3971,7 +3972,7 @@ "type": "tidelift" } ], - "time": "2026-01-13T10:40:19+00:00" + "time": "2026-02-17T07:53:42+00:00" }, { "name": "symfony/translation-contracts", @@ -4057,16 +4058,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8" + "reference": "8903bc9a64cf624ffe522893f3626d5a0b97175c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/f2dd26b604e856476ef7e0efa4568bc07eb7ddc8", - "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/8903bc9a64cf624ffe522893f3626d5a0b97175c", + "reference": "8903bc9a64cf624ffe522893f3626d5a0b97175c", "shasum": "" }, "require": { @@ -4076,7 +4077,7 @@ "twig/twig": "^3.21" }, "conflict": { - "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/reflection-docblock": "<5.2|>=7", "phpdocumentor/type-resolver": "<1.5.1", "symfony/console": "<6.4", "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", @@ -4090,7 +4091,7 @@ "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^5.2", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "symfony/asset": "^6.4|^7.0|^8.0", "symfony/asset-mapper": "^6.4|^7.0|^8.0", "symfony/console": "^6.4|^7.0|^8.0", @@ -4148,7 +4149,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v7.4.5" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.6" }, "funding": [ { @@ -4168,20 +4169,20 @@ "type": "tidelift" } ], - "time": "2026-01-27T08:59:58+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/type-info", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889" + "reference": "4855ceea609b2c09e48ff76e12a97a3955531735" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/f83c725e72b39b2704b9d6fc85070ad6ac7a5889", - "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889", + "url": "https://api.github.com/repos/symfony/type-info/zipball/4855ceea609b2c09e48ff76e12a97a3955531735", + "reference": "4855ceea609b2c09e48ff76e12a97a3955531735", "shasum": "" }, "require": { @@ -4231,7 +4232,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.4.4" + "source": "https://github.com/symfony/type-info/tree/v7.4.6" }, "funding": [ { @@ -4251,20 +4252,20 @@ "type": "tidelift" } ], - "time": "2026-01-09T12:14:21+00:00" + "time": "2026-02-17T14:00:31+00:00" }, { "name": "symfony/validator", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "fcec92c40df1c93507857da08226005573b655c6" + "reference": "a1ceaf285712ed8034819a76b5fbba23eaf3e54d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/fcec92c40df1c93507857da08226005573b655c6", - "reference": "fcec92c40df1c93507857da08226005573b655c6", + "url": "https://api.github.com/repos/symfony/validator/zipball/a1ceaf285712ed8034819a76b5fbba23eaf3e54d", + "reference": "a1ceaf285712ed8034819a76b5fbba23eaf3e54d", "shasum": "" }, "require": { @@ -4335,7 +4336,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.5" + "source": "https://github.com/symfony/validator/tree/v7.4.6" }, "funding": [ { @@ -4355,20 +4356,20 @@ "type": "tidelift" } ], - "time": "2026-01-27T08:59:58+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0e4769b46a0c3c62390d124635ce59f66874b282" + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282", - "reference": "0e4769b46a0c3c62390d124635ce59f66874b282", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", "shasum": "" }, "require": { @@ -4422,7 +4423,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" }, "funding": [ { @@ -4442,7 +4443,7 @@ "type": "tidelift" } ], - "time": "2026-01-01T22:13:48+00:00" + "time": "2026-02-15T10:53:20+00:00" }, { "name": "symfony/var-exporter", @@ -4527,16 +4528,16 @@ }, { "name": "symfony/yaml", - "version": "v7.4.1", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", - "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", "shasum": "" }, "require": { @@ -4579,7 +4580,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.1" + "source": "https://github.com/symfony/yaml/tree/v7.4.6" }, "funding": [ { @@ -4599,7 +4600,7 @@ "type": "tidelift" } ], - "time": "2025-12-04T18:11:45+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "twig/cache-extra", @@ -8934,16 +8935,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2" + "reference": "a3f7d594ca53a34a7d39ae683fbca09408b0c598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/76a02cddca45a5254479ad68f9fa274ead0a7ef2", - "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a3f7d594ca53a34a7d39ae683fbca09408b0c598", + "reference": "a3f7d594ca53a34a7d39ae683fbca09408b0c598", "shasum": "" }, "require": { @@ -8994,7 +8995,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.5" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.6" }, "funding": [ { @@ -9014,7 +9015,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:16:02+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/event-dispatcher", @@ -9544,5 +9545,5 @@ "platform-overrides": { "php": "8.2.99" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 5c4cbbd65529fbc470dc2ef8137bf91c8f759200 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Thu, 26 Feb 2026 14:37:58 +0100 Subject: [PATCH 03/11] Refactor section path handling and tree build Consolidate logic that ensures nested section paths and their ancestors exist by collecting all paths first and delegating creation to a new ensureSectionExists() helper. This removes inline duplication, uses Page::slugify to find index pages and determine language, and replaces the previous nestedSectionPaths flow. Also updated buildSectionTree() to drop the redundant nestedSectionPaths parameter and adjusted its call site accordingly. --- src/Generator/Section.php | 59 ++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/Generator/Section.php b/src/Generator/Section.php index c720115fd..3da00aca5 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -99,34 +99,23 @@ public function generate(): void $sections[$sectionPath][$language][] = $page; } - // Ensure all nested section paths exist in the sections map (even if empty). - // Also ensure their parent sections exist. - foreach ($nestedSectionPaths as $nestedPath => $_) { - if (!isset($sections[$nestedPath])) { - // Determine language from the index page - if ($this->builder->getPages()->has($nestedPath)) { - $indexPage = $this->builder->getPages()->get($nestedPath); - $lang = $indexPage->getVariable('language', $this->config->getLanguageDefault()); - $sections[$nestedPath][$lang] = []; - } - } - - // Walk up parent paths and ensure they exist as sections too. + // Ensure all nested section paths and their ancestors exist in the sections map (even if empty). + $pathsToEnsure = []; + foreach ($nestedSectionPaths as $nestedPath => $_) { // @SuppressWarnings(PHPMD.UnusedLocalVariable) + $pathsToEnsure[$nestedPath] = true; + // Collect ancestor paths. $parts = explode('/', $nestedPath); array_pop($parts); while (!empty($parts)) { - $parentPath = implode('/', $parts); - if (!isset($sections[$parentPath])) { - $parentSlug = Page::slugify($parentPath); - if ($this->builder->getPages()->has($parentSlug)) { - $indexPage = $this->builder->getPages()->get($parentSlug); - $lang = $indexPage->getVariable('language', $this->config->getLanguageDefault()); - $sections[$parentPath][$lang] = []; - } - } + $pathsToEnsure[implode('/', $parts)] = true; array_pop($parts); } } + foreach ($pathsToEnsure as $sectionPath => $_) { + if (!isset($sections[$sectionPath])) { + $this->ensureSectionExists($sections, $sectionPath); + } + } // Step 3: Create section pages. if (\count($sections) > 0) { @@ -207,7 +196,7 @@ public function generate(): void } // Step 4: Build parent/child relationships between sections. - $this->buildSectionTree($sectionPages, $nestedSectionPaths); + $this->buildSectionTree($sectionPages); } } @@ -292,10 +281,9 @@ protected function resolveSection(?string $originalFolder, string $rootSection, /** * Builds parent/child relationships between section pages. * - * @param array $sectionPages Map of "path|language" => section Page - * @param array $nestedSectionPaths Map of nested section paths + * @param array $sectionPages Map of "path|language" => section Page */ - protected function buildSectionTree(array $sectionPages, array $nestedSectionPaths): void + protected function buildSectionTree(array $sectionPages): void { foreach ($sectionPages as $key => $sectionPage) { [$path, $language] = explode('|', $key); @@ -339,6 +327,25 @@ protected function buildSectionTree(array $sectionPages, array $nestedSectionPat } } + /** + * Ensures that a section entry exists for the given path. + * + * Looks up the corresponding index page to determine the language, + * then creates an empty section entry. + * + * @param array>> &$sections The sections map (modified in-place) + * @param string $sectionPath The section path (already slugified) + */ + private function ensureSectionExists(array &$sections, string $sectionPath): void + { + $slug = Page::slugify($sectionPath); + if ($this->builder->getPages()->has($slug)) { + $lang = $this->builder->getPages()->get($slug) + ->getVariable('language', $this->config->getLanguageDefault()); + $sections[$sectionPath][$lang] = []; + } + } + /** * Adds navigation (next and prev) to each pages of a section. */ From afdaec0a9724fb249258852a6160693e55d1f416 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Thu, 26 Feb 2026 15:05:56 +0100 Subject: [PATCH 04/11] Only prepend section layouts when section exists Add a guard to ensure section-specific template names are only prepended if $section is truthy. Previously the code checked only $page->getPath(), which could lead to added layout paths with an empty section and unnecessary or invalid template lookups; this restricts those additions to when a section is present. --- src/Generator/Section.php | 2 +- src/Renderer/Layout.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Generator/Section.php b/src/Generator/Section.php index 3da00aca5..2099c702f 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -334,7 +334,7 @@ protected function buildSectionTree(array $sectionPages): void * then creates an empty section entry. * * @param array>> &$sections The sections map (modified in-place) - * @param string $sectionPath The section path (already slugified) + * @param string $sectionPath The section path (already slugified) */ private function ensureSectionExists(array &$sections, string $sectionPath): void { diff --git a/src/Renderer/Layout.php b/src/Renderer/Layout.php index 3cb2cc42f..fb5e639c2 100644 --- a/src/Renderer/Layout.php +++ b/src/Renderer/Layout.php @@ -123,7 +123,7 @@ protected static function lookup(CollectionPage $page, string $format, \Cecil\Co "list.$format.$ext", "_default/list.$format.$ext", ]; - if ($page->getPath()) { + if ($section && $page->getPath()) { $layouts = array_merge(["section/{$section}.$format.$ext"], $layouts); $layouts = array_merge(["{$section}/list.$format.$ext"], $layouts); $layouts = array_merge(["{$section}/index.$format.$ext"], $layouts); From 48174c61f5fcee71bb37cd34416cb725e862d0a8 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Mon, 2 Mar 2026 00:59:46 +0100 Subject: [PATCH 05/11] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Generator/Section.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Generator/Section.php b/src/Generator/Section.php index 2099c702f..67c077228 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -41,9 +41,14 @@ class Section extends AbstractGenerator implements GeneratorInterface */ public function generate(): void { - // Step 1: Detect nested section paths (subfolders with index.md). - // Returns a map of slugified-folder-path => page-id. - $nestedSectionPaths = $this->detectNestedSectionPaths(); + // Step 1: Detect nested section paths (subfolders with index.md), + // which are only enabled when `pages.sections.nested` is set to true + // in the configuration. + $nestedSectionPaths = []; + if ((bool) $this->config->get('pages.sections.nested', false) === true) { + // Returns a map of slugified-folder-path => page-id. + $nestedSectionPaths = $this->detectNestedSectionPaths(); + } // Build a reverse map: page-id => folder-path (for looking up a page's original folder). $pageIdToFolderPath = []; From 8308c112adee1e4dfc8c3f67306c32e908a1d098 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Mon, 2 Mar 2026 01:00:13 +0100 Subject: [PATCH 06/11] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Generator/Section.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Generator/Section.php b/src/Generator/Section.php index 67c077228..f763cc2df 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -174,8 +174,12 @@ public function generate(): void ->setPages($pages) ->setVariable('language', $language) ->setVariable('langref', $path); - if ($pages->first()) { - $page->setVariable('date', $pages->first()->getVariable('date')); + $firstPage = $pages->first(); + if ($firstPage instanceof Page && $firstPage->hasVariable('date')) { + $page->setVariable('date', $firstPage->getVariable('date')); + } else { + // Ensure the section always has a 'date' variable, even if it has no direct pages + $page->setVariable('date', null); } // human readable title if ($page->getVariable('title') == 'index') { From 25c51e50c20011034fa9111da359659778b69668 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Mon, 2 Mar 2026 01:02:19 +0100 Subject: [PATCH 07/11] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Generator/Section.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Generator/Section.php b/src/Generator/Section.php index f763cc2df..97230753f 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -348,9 +348,18 @@ protected function buildSectionTree(array $sectionPages): void private function ensureSectionExists(array &$sections, string $sectionPath): void { $slug = Page::slugify($sectionPath); + + // Determine the language for this section. Prefer the language from an existing + // index page when available; otherwise, fall back to the default language. if ($this->builder->getPages()->has($slug)) { $lang = $this->builder->getPages()->get($slug) ->getVariable('language', $this->config->getLanguageDefault()); + } else { + $lang = $this->config->getLanguageDefault(); + } + + // Ensure the section entry exists for the resolved language. + if (!isset($sections[$sectionPath][$lang])) { $sections[$sectionPath][$lang] = []; } } From 361b72e69b8b505920c2846b7a9cefe2042f9efa Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Sun, 5 Apr 2026 20:42:08 +0200 Subject: [PATCH 08/11] chore: update deps --- composer.lock | 399 +++++++++++++++++++++++++------------------------- 1 file changed, 200 insertions(+), 199 deletions(-) diff --git a/composer.lock b/composer.lock index 576b37187..f39dd9da6 100644 --- a/composer.lock +++ b/composer.lock @@ -549,16 +549,16 @@ }, { "name": "james-heinrich/getid3", - "version": "v1.9.24", + "version": "v1.9.25", "source": { "type": "git", "url": "https://github.com/JamesHeinrich/getID3.git", - "reference": "1e11b9b6eb468b522fe604a42a9fdc8ee759bf8a" + "reference": "fefffe762b02be155dcc32eec57feff8a49bc4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/1e11b9b6eb468b522fe604a42a9fdc8ee759bf8a", - "reference": "1e11b9b6eb468b522fe604a42a9fdc8ee759bf8a", + "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/fefffe762b02be155dcc32eec57feff8a49bc4b5", + "reference": "fefffe762b02be155dcc32eec57feff8a49bc4b5", "shasum": "" }, "require": { @@ -610,9 +610,9 @@ ], "support": { "issues": "https://github.com/JamesHeinrich/getID3/issues", - "source": "https://github.com/JamesHeinrich/getID3/tree/v1.9.24" + "source": "https://github.com/JamesHeinrich/getID3/tree/v1.9.25" }, - "time": "2025-10-09T17:48:17+00:00" + "time": "2026-03-10T11:56:47+00:00" }, { "name": "jolicode/jolinotif", @@ -800,20 +800,20 @@ }, { "name": "league/uri", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8", + "league/uri-interfaces": "^7.8.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -886,7 +886,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -894,20 +894,20 @@ "type": "github" } ], - "time": "2026-01-14T17:24:56+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { @@ -970,7 +970,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -978,7 +978,7 @@ "type": "github" } ], - "time": "2026-01-15T06:54:53+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "masterminds/html5", @@ -1806,16 +1806,16 @@ }, { "name": "symfony/cache", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "1d06192e8f164e2729b0031e6807d72a6195b8bb" + "reference": "467464da294734b0fb17e853e5712abc8470f819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/1d06192e8f164e2729b0031e6807d72a6195b8bb", - "reference": "1d06192e8f164e2729b0031e6807d72a6195b8bb", + "url": "https://api.github.com/repos/symfony/cache/zipball/467464da294734b0fb17e853e5712abc8470f819", + "reference": "467464da294734b0fb17e853e5712abc8470f819", "shasum": "" }, "require": { @@ -1886,7 +1886,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.6" + "source": "https://github.com/symfony/cache/tree/v7.4.8" }, "funding": [ { @@ -1906,7 +1906,7 @@ "type": "tidelift" } ], - "time": "2026-02-21T23:29:27+00:00" + "time": "2026-03-30T15:15:47+00:00" }, { "name": "symfony/cache-contracts", @@ -1986,16 +1986,16 @@ }, { "name": "symfony/config", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "9400e2f9226b3b64ebb0a8ae967ae84e54e39640" + "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/9400e2f9226b3b64ebb0a8ae967ae84e54e39640", - "reference": "9400e2f9226b3b64ebb0a8ae967ae84e54e39640", + "url": "https://api.github.com/repos/symfony/config/zipball/2d19dde43fa2ff720b9a40763ace7226594f503b", + "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b", "shasum": "" }, "require": { @@ -2041,7 +2041,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.6" + "source": "https://github.com/symfony/config/tree/v7.4.8" }, "funding": [ { @@ -2061,20 +2061,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/console", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d643a93b47398599124022eb24d97c153c12f27" + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", - "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", "shasum": "" }, "require": { @@ -2139,7 +2139,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.6" + "source": "https://github.com/symfony/console/tree/v7.4.8" }, "funding": [ { @@ -2159,20 +2159,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T17:02:47+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/css-selector", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "2e7c52c647b406e2107dd867db424a4dbac91864" + "reference": "b055f228a4178a1d6774909903905e3475f3eac8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/2e7c52c647b406e2107dd867db424a4dbac91864", - "reference": "2e7c52c647b406e2107dd867db424a4dbac91864", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8", + "reference": "b055f228a4178a1d6774909903905e3475f3eac8", "shasum": "" }, "require": { @@ -2208,7 +2208,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.6" + "source": "https://github.com/symfony/css-selector/tree/v7.4.8" }, "funding": [ { @@ -2228,7 +2228,7 @@ "type": "tidelift" } ], - "time": "2026-02-17T07:53:42+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2299,16 +2299,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e" + "reference": "2918e7c2ba964defca1f5b69c6f74886529e2dc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/487ba8fa43da9a8e6503fe939b45ecd96875410e", - "reference": "487ba8fa43da9a8e6503fe939b45ecd96875410e", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2918e7c2ba964defca1f5b69c6f74886529e2dc8", + "reference": "2918e7c2ba964defca1f5b69c6f74886529e2dc8", "shasum": "" }, "require": { @@ -2347,7 +2347,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.4.6" + "source": "https://github.com/symfony/dom-crawler/tree/v7.4.8" }, "funding": [ { @@ -2367,20 +2367,20 @@ "type": "tidelift" } ], - "time": "2026-02-17T07:53:42+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", "shasum": "" }, "require": { @@ -2417,7 +2417,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.8" }, "funding": [ { @@ -2437,20 +2437,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/finder", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { @@ -2485,7 +2485,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.6" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -2505,20 +2505,20 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:40:50+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/intl", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "6d6a398b18f73b3110140dbb030dcee2ae4ea81f" + "reference": "7cfb7792d580dea833647420afd5f2f98df8457b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/6d6a398b18f73b3110140dbb030dcee2ae4ea81f", - "reference": "6d6a398b18f73b3110140dbb030dcee2ae4ea81f", + "url": "https://api.github.com/repos/symfony/intl/zipball/7cfb7792d580dea833647420afd5f2f98df8457b", + "reference": "7cfb7792d580dea833647420afd5f2f98df8457b", "shasum": "" }, "require": { @@ -2575,7 +2575,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v7.4.6" + "source": "https://github.com/symfony/intl/tree/v7.4.8" }, "funding": [ { @@ -2595,20 +2595,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T09:33:46+00:00" + "time": "2026-03-30T12:55:43+00:00" }, { "name": "symfony/mime", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f" + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", - "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "url": "https://api.github.com/repos/symfony/mime/zipball/6df02f99998081032da3407a8d6c4e1dcb5d4379", + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379", "shasum": "" }, "require": { @@ -2664,7 +2664,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.6" + "source": "https://github.com/symfony/mime/tree/v7.4.8" }, "funding": [ { @@ -2684,7 +2684,7 @@ "type": "tidelift" } ], - "time": "2026-02-05T15:57:06+00:00" + "time": "2026-03-30T14:11:46+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3358,16 +3358,16 @@ }, { "name": "symfony/process", - "version": "v7.4.5", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", "shasum": "" }, "require": { @@ -3399,7 +3399,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" + "source": "https://github.com/symfony/process/tree/v7.4.8" }, "funding": [ { @@ -3419,20 +3419,20 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/property-access", - "version": "v7.4.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1" + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", - "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", + "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", "shasum": "" }, "require": { @@ -3480,7 +3480,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.4.4" + "source": "https://github.com/symfony/property-access/tree/v7.4.8" }, "funding": [ { @@ -3500,27 +3500,27 @@ "type": "tidelift" } ], - "time": "2026-01-05T08:47:25+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/property-info", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "6396b28f44d7c28b209a1bd73acf0dd985a0a4ef" + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/6396b28f44d7c28b209a1bd73acf0dd985a0a4ef", - "reference": "6396b28f44d7c28b209a1bd73acf0dd985a0a4ef", + "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/string": "^6.4|^7.0|^8.0", - "symfony/type-info": "~7.3.10|^7.4.4|^8.0.4" + "symfony/type-info": "^7.4.7|^8.0.7" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2|>=7", @@ -3570,7 +3570,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.4.6" + "source": "https://github.com/symfony/property-info/tree/v7.4.8" }, "funding": [ { @@ -3590,20 +3590,20 @@ "type": "tidelift" } ], - "time": "2026-02-13T11:51:31+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/serializer", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "83c3cbd6dcb96c1dbe197499a0714f8dceb0f274" + "reference": "006fd51717addf2df2bd1a64dafef6b7fab6b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/83c3cbd6dcb96c1dbe197499a0714f8dceb0f274", - "reference": "83c3cbd6dcb96c1dbe197499a0714f8dceb0f274", + "url": "https://api.github.com/repos/symfony/serializer/zipball/006fd51717addf2df2bd1a64dafef6b7fab6b455", + "reference": "006fd51717addf2df2bd1a64dafef6b7fab6b455", "shasum": "" }, "require": { @@ -3674,7 +3674,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.4.6" + "source": "https://github.com/symfony/serializer/tree/v7.4.8" }, "funding": [ { @@ -3694,7 +3694,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-30T21:34:42+00:00" }, { "name": "symfony/service-contracts", @@ -3785,16 +3785,16 @@ }, { "name": "symfony/string", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" + "reference": "114ac57257d75df748eda23dd003878080b8e688" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", - "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", + "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", + "reference": "114ac57257d75df748eda23dd003878080b8e688", "shasum": "" }, "require": { @@ -3852,7 +3852,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.6" + "source": "https://github.com/symfony/string/tree/v7.4.8" }, "funding": [ { @@ -3872,20 +3872,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T09:33:46+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/translation", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "1888cf064399868af3784b9e043240f1d89d25ce" + "reference": "33600f8489485425bfcddd0d983391038d3422e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce", - "reference": "1888cf064399868af3784b9e043240f1d89d25ce", + "url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7", + "reference": "33600f8489485425bfcddd0d983391038d3422e7", "shasum": "" }, "require": { @@ -3952,7 +3952,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.6" + "source": "https://github.com/symfony/translation/tree/v7.4.8" }, "funding": [ { @@ -3972,7 +3972,7 @@ "type": "tidelift" } ], - "time": "2026-02-17T07:53:42+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/translation-contracts", @@ -4058,16 +4058,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "8903bc9a64cf624ffe522893f3626d5a0b97175c" + "reference": "ac43e7e59298ed1ce98c8d228b651d46e907d02c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/8903bc9a64cf624ffe522893f3626d5a0b97175c", - "reference": "8903bc9a64cf624ffe522893f3626d5a0b97175c", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/ac43e7e59298ed1ce98c8d228b651d46e907d02c", + "reference": "ac43e7e59298ed1ce98c8d228b651d46e907d02c", "shasum": "" }, "require": { @@ -4083,7 +4083,7 @@ "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", "symfony/http-foundation": "<6.4", "symfony/http-kernel": "<6.4", - "symfony/mime": "<6.4", + "symfony/mime": "<6.4.36|>7,<7.4.8|>8.0,<8.0.8", "symfony/serializer": "<6.4", "symfony/translation": "<6.4", "symfony/workflow": "<6.4" @@ -4104,7 +4104,7 @@ "symfony/http-foundation": "^7.3|^8.0", "symfony/http-kernel": "^6.4|^7.0|^8.0", "symfony/intl": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4.36|^7.4.8|^8.0.8", "symfony/polyfill-intl-icu": "~1.0", "symfony/property-info": "^6.4|^7.0|^8.0", "symfony/routing": "^6.4|^7.0|^8.0", @@ -4149,7 +4149,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v7.4.6" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.8" }, "funding": [ { @@ -4169,20 +4169,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-30T15:17:09+00:00" }, { "name": "symfony/type-info", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "4855ceea609b2c09e48ff76e12a97a3955531735" + "reference": "6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/4855ceea609b2c09e48ff76e12a97a3955531735", - "reference": "4855ceea609b2c09e48ff76e12a97a3955531735", + "url": "https://api.github.com/repos/symfony/type-info/zipball/6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd", + "reference": "6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd", "shasum": "" }, "require": { @@ -4232,7 +4232,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.4.6" + "source": "https://github.com/symfony/type-info/tree/v7.4.8" }, "funding": [ { @@ -4252,20 +4252,20 @@ "type": "tidelift" } ], - "time": "2026-02-17T14:00:31+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/validator", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "a1ceaf285712ed8034819a76b5fbba23eaf3e54d" + "reference": "8f73cbddae916756f319b3e195088da216f0f12f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/a1ceaf285712ed8034819a76b5fbba23eaf3e54d", - "reference": "a1ceaf285712ed8034819a76b5fbba23eaf3e54d", + "url": "https://api.github.com/repos/symfony/validator/zipball/8f73cbddae916756f319b3e195088da216f0f12f", + "reference": "8f73cbddae916756f319b3e195088da216f0f12f", "shasum": "" }, "require": { @@ -4336,7 +4336,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.6" + "source": "https://github.com/symfony/validator/tree/v7.4.8" }, "funding": [ { @@ -4356,20 +4356,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-30T12:55:43+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", "shasum": "" }, "require": { @@ -4423,7 +4423,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" }, "funding": [ { @@ -4443,20 +4443,20 @@ "type": "tidelift" } ], - "time": "2026-02-15T10:53:20+00:00" + "time": "2026-03-30T13:44:50+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/398907e89a2a56fe426f7955c6fa943ec0c77225", + "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225", "shasum": "" }, "require": { @@ -4504,7 +4504,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.8" }, "funding": [ { @@ -4524,20 +4524,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", - "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", "shasum": "" }, "require": { @@ -4580,7 +4580,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.6" + "source": "https://github.com/symfony/yaml/tree/v7.4.8" }, "funding": [ { @@ -4600,20 +4600,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T09:33:46+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "twig/cache-extra", - "version": "v3.23.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/cache-extra.git", - "reference": "c85c6ca1142db2f1f4f8c853988aef560d064199" + "reference": "573e22cb5fe3da13ea785d28dee33a2a88e33d53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/cache-extra/zipball/c85c6ca1142db2f1f4f8c853988aef560d064199", - "reference": "c85c6ca1142db2f1f4f8c853988aef560d064199", + "url": "https://api.github.com/repos/twigphp/cache-extra/zipball/573e22cb5fe3da13ea785d28dee33a2a88e33d53", + "reference": "573e22cb5fe3da13ea785d28dee33a2a88e33d53", "shasum": "" }, "require": { @@ -4653,7 +4653,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/cache-extra/tree/v3.23.0" + "source": "https://github.com/twigphp/cache-extra/tree/v3.24.0" }, "funding": [ { @@ -4665,11 +4665,11 @@ "type": "tidelift" } ], - "time": "2025-12-02T14:45:16+00:00" + "time": "2026-02-07T08:07:38+00:00" }, { "name": "twig/intl-extra", - "version": "v3.23.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", @@ -4717,7 +4717,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/intl-extra/tree/v3.23.0" + "source": "https://github.com/twigphp/intl-extra/tree/v3.24.0" }, "funding": [ { @@ -4733,7 +4733,7 @@ }, { "name": "twig/string-extra", - "version": "v3.23.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", @@ -4784,7 +4784,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.23.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.24.0" }, "funding": [ { @@ -4800,16 +4800,16 @@ }, { "name": "twig/twig", - "version": "v3.23.0", + "version": "v3.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" + "reference": "a6769aefb305efef849dc25c9fd1653358c148f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", - "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6769aefb305efef849dc25c9fd1653358c148f0", + "reference": "a6769aefb305efef849dc25c9fd1653358c148f0", "shasum": "" }, "require": { @@ -4819,7 +4819,8 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "phpstan/phpstan": "^2.0", + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -4863,7 +4864,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.23.0" + "source": "https://github.com/twigphp/Twig/tree/v3.24.0" }, "funding": [ { @@ -4875,7 +4876,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T21:00:41+00:00" + "time": "2026-03-17T21:31:11+00:00" }, { "name": "voku/html-min", @@ -6147,16 +6148,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "v6.7.2", + "version": "6.8.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0" + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", "shasum": "" }, "require": { @@ -6216,9 +6217,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.0" }, - "time": "2026-02-15T15:06:22+00:00" + "time": "2026-04-02T12:43:11+00:00" }, { "name": "localheinz/diff", @@ -6732,11 +6733,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.40", + "version": "2.1.46", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", - "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", "shasum": "" }, "require": { @@ -6781,7 +6782,7 @@ "type": "github" } ], - "time": "2026-02-23T15:04:35+00:00" + "time": "2026-04-01T09:25:14+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8935,16 +8936,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "a3f7d594ca53a34a7d39ae683fbca09408b0c598" + "reference": "f7025fd7b687c240426562f86ada06a93b1e771d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a3f7d594ca53a34a7d39ae683fbca09408b0c598", - "reference": "a3f7d594ca53a34a7d39ae683fbca09408b0c598", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f7025fd7b687c240426562f86ada06a93b1e771d", + "reference": "f7025fd7b687c240426562f86ada06a93b1e771d", "shasum": "" }, "require": { @@ -8995,7 +8996,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.6" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.8" }, "funding": [ { @@ -9015,20 +9016,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-31T06:50:29+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.4", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "dc2c0eba1af673e736bb851d747d266108aea746" + "reference": "f57b899fa736fd71121168ef268f23c206083f0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", - "reference": "dc2c0eba1af673e736bb851d747d266108aea746", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a", "shasum": "" }, "require": { @@ -9080,7 +9081,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" }, "funding": [ { @@ -9100,7 +9101,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T11:45:34+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -9180,16 +9181,16 @@ }, { "name": "symfony/options-resolver", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b38026df55197f9e39a44f3215788edf83187b80" + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", - "reference": "b38026df55197f9e39a44f3215788edf83187b80", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", "shasum": "" }, "require": { @@ -9227,7 +9228,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" }, "funding": [ { @@ -9247,7 +9248,7 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:39:26+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-php80", @@ -9415,16 +9416,16 @@ }, { "name": "symfony/stopwatch", - "version": "v7.4.0", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "8a24af0a2e8a872fb745047180649b8418303084" + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084", - "reference": "8a24af0a2e8a872fb745047180649b8418303084", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89", "shasum": "" }, "require": { @@ -9457,7 +9458,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.4.0" + "source": "https://github.com/symfony/stopwatch/tree/v7.4.8" }, "funding": [ { @@ -9477,7 +9478,7 @@ "type": "tidelift" } ], - "time": "2025-08-04T07:05:15+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "theseer/tokenizer", From 36694c92dc770cf5db55089839f876235848181b Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Sun, 5 Apr 2026 20:42:49 +0200 Subject: [PATCH 09/11] fix checking pages.sections.nested --- src/Generator/Section.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Generator/Section.php b/src/Generator/Section.php index 97230753f..d125e4cf9 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -45,7 +45,7 @@ public function generate(): void // which are only enabled when `pages.sections.nested` is set to true // in the configuration. $nestedSectionPaths = []; - if ((bool) $this->config->get('pages.sections.nested', false) === true) { + if ((bool) $this->config->get('pages.sections.nested') === true) { // Returns a map of slugified-folder-path => page-id. $nestedSectionPaths = $this->detectNestedSectionPaths(); } From 983175473e23caedaa3ea821c7b154a52c8e7eb0 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 22 Apr 2026 00:46:53 +0200 Subject: [PATCH 10/11] chore: update deps --- composer.lock | 266 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 169 insertions(+), 97 deletions(-) diff --git a/composer.lock b/composer.lock index f39dd9da6..808f04122 100644 --- a/composer.lock +++ b/composer.lock @@ -2688,16 +2688,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -2747,7 +2747,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" }, "funding": [ { @@ -2767,20 +2767,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", "shasum": "" }, "require": { @@ -2829,7 +2829,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" }, "funding": [ { @@ -2849,20 +2849,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" + "reference": "3510b63d07376b04e57e27e82607d468bb134f78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", - "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/3510b63d07376b04e57e27e82607d468bb134f78", + "reference": "3510b63d07376b04e57e27e82607d468bb134f78", "shasum": "" }, "require": { @@ -2917,7 +2917,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.36.0" }, "funding": [ { @@ -2937,11 +2937,11 @@ "type": "tidelift" } ], - "time": "2025-06-20T22:24:30+00:00" + "time": "2026-04-10T16:50:15+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -3004,7 +3004,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0" }, "funding": [ { @@ -3028,7 +3028,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3089,7 +3089,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" }, "funding": [ { @@ -3113,16 +3113,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -3174,7 +3174,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" }, "funding": [ { @@ -3194,20 +3194,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", "shasum": "" }, "require": { @@ -3254,7 +3254,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0" }, "funding": [ { @@ -3274,20 +3274,20 @@ "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", "shasum": "" }, "require": { @@ -3334,7 +3334,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0" }, "funding": [ { @@ -3354,7 +3354,7 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-04-10T18:47:49+00:00" }, { "name": "symfony/process", @@ -5470,18 +5470,87 @@ ], "time": "2024-05-06T16:37:16+00:00" }, + { + "name": "ergebnis/agent-detector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", + "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.50.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.16.0", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.46", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-04-10T13:45:13+00:00" + }, { "name": "ergebnis/composer-normalize", - "version": "2.50.0", + "version": "2.51.0", "source": { "type": "git", "url": "https://github.com/ergebnis/composer-normalize.git", - "reference": "80971fe24ff10709789942bcbe9368b2c704097c" + "reference": "36fb17dce18579ccab50f71b411a32ed55e6d4bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/80971fe24ff10709789942bcbe9368b2c704097c", - "reference": "80971fe24ff10709789942bcbe9368b2c704097c", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/36fb17dce18579ccab50f71b411a32ed55e6d4bc", + "reference": "36fb17dce18579ccab50f71b411a32ed55e6d4bc", "shasum": "" }, "require": { @@ -5497,25 +5566,25 @@ "require-dev": { "composer/composer": "^2.9.4", "ergebnis/license": "^2.7.0", - "ergebnis/php-cs-fixer-config": "^6.59.0", + "ergebnis/php-cs-fixer-config": "^6.61.1", "ergebnis/phpstan-rules": "^2.13.1", - "ergebnis/phpunit-slow-test-detector": "^2.20.0", - "ergebnis/rector-rules": "^1.9.0", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", "fakerphp/faker": "^1.24.1", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.38", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.12", - "phpstan/phpstan-strict-rules": "^2.0.8", + "phpstan/phpstan": "^2.1.47", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", "phpunit/phpunit": "^9.6.33", - "rector/rector": "^2.3.5", + "rector/rector": "^2.4.1", "symfony/filesystem": "^5.4.41" }, "type": "composer-plugin", "extra": { "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin", "branch-alias": { - "dev-main": "2.49-dev" + "dev-main": "2.51-dev" }, "plugin-optional": true, "composer-normalize": { @@ -5552,7 +5621,7 @@ "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/composer-normalize" }, - "time": "2026-02-09T20:57:47+00:00" + "time": "2026-04-14T11:17:04+00:00" }, { "name": "ergebnis/json", @@ -5711,36 +5780,38 @@ }, { "name": "ergebnis/json-pointer", - "version": "3.7.1", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/ergebnis/json-pointer.git", - "reference": "43bef355184e9542635e35dd2705910a3df4c236" + "reference": "b58c3c468a7ff109fdf9a255f17de29ecbe5276c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/43bef355184e9542635e35dd2705910a3df4c236", - "reference": "43bef355184e9542635e35dd2705910a3df4c236", + "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/b58c3c468a7ff109fdf9a255f17de29ecbe5276c", + "reference": "b58c3c468a7ff109fdf9a255f17de29ecbe5276c", "shasum": "" }, "require": { "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { - "ergebnis/composer-normalize": "^2.43.0", - "ergebnis/data-provider": "^3.2.0", - "ergebnis/license": "^2.4.0", - "ergebnis/php-cs-fixer-config": "^6.32.0", - "ergebnis/phpunit-slow-test-detector": "^2.15.0", - "fakerphp/faker": "^1.23.1", + "ergebnis/composer-normalize": "^2.50.0", + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.16.0", + "fakerphp/faker": "^1.24.1", "infection/infection": "~0.26.6", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.10", - "phpstan/phpstan-deprecation-rules": "^1.2.1", - "phpstan/phpstan-phpunit": "^1.4.0", - "phpstan/phpstan-strict-rules": "^1.6.1", - "phpunit/phpunit": "^9.6.19", - "rector/rector": "^1.2.10" + "phpstan/phpstan": "^2.1.46", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.0" }, "type": "library", "extra": { @@ -5780,7 +5851,7 @@ "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/json-pointer" }, - "time": "2025-09-06T09:28:19+00:00" + "time": "2026-04-07T14:52:13+00:00" }, { "name": "ergebnis/json-printer", @@ -6044,22 +6115,23 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.94.2", + "version": "v3.95.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63" + "reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63", - "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a9727678fbd12997f1d9de8f4a37824ed9df1065", + "reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065", "shasum": "" }, "require": { "clue/ndjson-react": "^1.3", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.1.1", "ext-filter": "*", "ext-hash": "*", "ext-json": "*", @@ -6084,18 +6156,18 @@ "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.7.1", - "infection/infection": "^0.32.3", - "justinrainbow/json-schema": "^6.6.4", + "facile-it/paraunit": "^1.3.1 || ^2.8.0", + "infection/infection": "^0.32.6", + "justinrainbow/json-schema": "^6.8.0", "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.9.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7", - "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", "symfony/polyfill-php85": "^1.33", - "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1" + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.8" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -6136,7 +6208,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.1" }, "funding": [ { @@ -6144,7 +6216,7 @@ "type": "github" } ], - "time": "2026-02-20T16:13:53+00:00" + "time": "2026-04-12T17:00:09+00:00" }, { "name": "justinrainbow/json-schema", @@ -6733,11 +6805,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.46", + "version": "2.1.51", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", - "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", + "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", "shasum": "" }, "require": { @@ -6782,7 +6854,7 @@ "type": "github" } ], - "time": "2026-04-01T09:25:14+00:00" + "time": "2026-04-21T18:22:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -9252,16 +9324,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -9312,7 +9384,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.36.0" }, "funding": [ { @@ -9332,11 +9404,11 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -9392,7 +9464,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.36.0" }, "funding": [ { From 0aac27bf184678668c1b33771ff5fae5ee20bff8 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 22 Apr 2026 00:50:39 +0200 Subject: [PATCH 11/11] Update composer.lock --- composer.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/composer.lock b/composer.lock index 77aeb0636..bc06bf486 100644 --- a/composer.lock +++ b/composer.lock @@ -2614,7 +2614,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -2673,7 +2673,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" }, "funding": [ { @@ -2697,7 +2697,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -2755,7 +2755,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" }, "funding": [ { @@ -2867,7 +2867,7 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -2930,7 +2930,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0" }, "funding": [ { @@ -2954,7 +2954,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3015,7 +3015,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" }, "funding": [ { @@ -3039,7 +3039,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -3100,7 +3100,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" }, "funding": [ { @@ -3124,7 +3124,7 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -3180,7 +3180,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0" }, "funding": [ { @@ -3204,7 +3204,7 @@ }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", @@ -3260,7 +3260,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0" }, "funding": [ { @@ -6731,11 +6731,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.46", + "version": "2.1.51", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", - "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", + "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", "shasum": "" }, "require": { @@ -6780,7 +6780,7 @@ "type": "github" } ], - "time": "2026-04-01T09:25:14+00:00" + "time": "2026-04-21T18:22:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -9250,7 +9250,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -9310,7 +9310,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.36.0" }, "funding": [ { @@ -9334,7 +9334,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -9390,7 +9390,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.36.0" }, "funding": [ {