diff --git a/resources/layouts/_default/page.html.twig b/resources/layouts/_default/page.html.twig index f88b30d57..cc5ad475f 100644 --- a/resources/layouts/_default/page.html.twig +++ b/resources/layouts/_default/page.html.twig @@ -52,6 +52,22 @@ nav a:hover:not(main) { text-decoration: underline; } + nav.breadcrumb ol { + display: flex; + flex-wrap: wrap; + list-style: none; + margin: 0; + padding: 0; + } + nav.breadcrumb li { + margin: 0; + font-size: 0.875rem; + } + nav.breadcrumb li:not(:last-child)::after { + margin: .25rem; + content: "›"; + opacity: .5; + } main header { background: unset; border-bottom: unset; @@ -157,6 +173,7 @@ {%- endblock header ~%}
+ {%- include 'partials/breadcrumb.html.twig' with {page: page} only ~%} {%- block content ~%} {{ page.content }} {%- endblock content ~%} diff --git a/resources/layouts/partials/breadcrumb.html.twig b/resources/layouts/partials/breadcrumb.html.twig new file mode 100644 index 000000000..f53e014b1 --- /dev/null +++ b/resources/layouts/partials/breadcrumb.html.twig @@ -0,0 +1,19 @@ + {%- if page.type != 'homepage' and page.ancestors|default([])|length > 0 ~%} + + {%- endif ~%} \ No newline at end of file diff --git a/src/Collection/Page/Page.php b/src/Collection/Page/Page.php index 53d142aa1..1126352a2 100644 --- a/src/Collection/Page/Page.php +++ b/src/Collection/Page/Page.php @@ -71,6 +71,9 @@ class Page extends Item /** @var \Cecil\Collection\Taxonomy\Vocabulary Terms of a vocabulary. */ protected $terms; + /** @var self Parent page of a PAGE page or a SECTION page */ + protected $parent; + /** @var Slugify */ private static $slugifier; @@ -118,20 +121,18 @@ public static function slugify(string $path): string */ public static function createIdFromFile(SplFileInfo $file): string { - $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath())); - $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension()))); - // if file is "README.md", ID is "index" - $basename = (string) str_ireplace('readme', 'index', $basename); - // if file is section's index: "section/index.md", ID is "section" + $relativePath = self::slugify(self::getFileComponents($file)['path']); + $basename = self::slugify(PrefixSuffix::subPrefix(self::getFileComponents($file)['name'])); + // if file is a section's index: "
/index.md", "
" is the ID if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') { - // case of a localized section's index: "section/index.fr.md", ID is "fr/section" + // case of a localized section's index: "
/index.fr.md", "" is the ID if (PrefixSuffix::hasSuffix($basename)) { return PrefixSuffix::getSuffix($basename) . '/' . $relativePath; } return $relativePath; } - // localized page + // localized page: ".fr.md" -> "fr/" if (PrefixSuffix::hasSuffix($basename)) { return trim(Util::joinPath(PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename)), '/'); } @@ -163,15 +164,8 @@ public function setFile(SplFileInfo $file): self /* * File path components */ - $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath()); - $fileExtension = $this->file->getExtension(); - $fileName = $this->file->getBasename('.' . $fileExtension); - // renames "README" to "index" - $fileName = (string) str_ireplace('readme', 'index', $fileName); - // case of "index" = home page - if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName) == 'index') { - $this->setType(Type::HOMEPAGE->value); - } + $fileRelativePath = self::getFileComponents($file)['path']; + $fileName = self::getFileComponents($file)['name']; /* * Set page properties and variables */ @@ -184,9 +178,16 @@ public function setFile(SplFileInfo $file): self 'updated' => (new \DateTime())->setTimestamp($this->file->getMTime()), 'filepath' => $this->file->getRelativePathname(), ]); - /* - * Set specific variables - */ + // is a section? + if (PrefixSuffix::sub($fileName) == 'index') { + $this->setType(Type::SECTION->value); + $this->setVariable('title', ucfirst(explode('/', $fileRelativePath)[\count(explode('/', $fileRelativePath)) - 1])); + // is the home page? + if (empty($this->getFolder())) { + $this->setType(Type::HOMEPAGE->value); + $this->setVariable('title', 'Homepage'); + } + } // is file has a prefix? if (PrefixSuffix::hasPrefix($fileName)) { $prefix = PrefixSuffix::getPrefix($fileName); @@ -336,29 +337,22 @@ public function getSlug(): string public function setPath(string $path): self { $path = trim($path, '/'); - // case of homepage if ($path == 'index') { $this->path = ''; - return $this; } - // case of custom sections' index (ie: section/index.md -> section) if (substr($path, -6) == '/index') { $path = substr($path, 0, \strlen($path) - 6); } $this->path = $path; - $lastslash = strrpos($this->path, '/'); - // case of root/top-level pages if ($lastslash === false) { $this->slug = $this->path; - return $this; } - // case of sections' pages: set section if (!$this->virtual && $this->getSection() === null) { $this->section = explode('/', $this->path)[0]; @@ -366,7 +360,6 @@ public function setPath(string $path): self // set/update folder and slug $this->folder = substr($this->path, 0, $lastslash); $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1)); - return $this; } @@ -665,6 +658,46 @@ public function getFmVariables(): array return $this->fmVariables; } + /** + * Set parent page. + */ + public function setParent(self $page): self + { + $this->parent = $page; + + return $this; + } + + /** + * Returns parent page if exists. + */ + public function getParent(): ?self + { + return $this->parent; + } + + /** + * Returns array of ancestors pages. + */ + public function getAncestors(): array + { + $ancestors = []; + $currentPage = $this; + while ($currentPage->getParent() !== null) { + $ancestors[] = $currentPage = $currentPage->getParent(); + } + + return $ancestors; + } + + /** + * {@inheritdoc} + */ + public function setId(string $id): self + { + return parent::setId($id); + } + /** * Cast "boolean" string (or array of strings) to boolean. * @@ -683,10 +716,20 @@ private function filterBool(&$value) } /** - * {@inheritdoc} - */ - public function setId(string $id): self - { - return parent::setId($id); + * Get file components. + * + * [ + * path => relative path, + * name => name, + * ext => extension, + * ] + */ + private static function getFileComponents(SplFileInfo $file): array + { + return [ + 'path' => str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()), + 'name' => (string) str_ireplace('readme', 'index', $file->getBasename('.' . $file->getExtension())), + 'ext' => $file->getExtension(), + ]; } } diff --git a/src/Generator/Section.php b/src/Generator/Section.php index 5b78bff3f..0dca399d5 100644 --- a/src/Generator/Section.php +++ b/src/Generator/Section.php @@ -30,16 +30,21 @@ public function generate(): void { $sections = []; - // identifying sections from all pages + // identifying sections from pages collection /** @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 + // do not add "draft" and "excluded" pages to its section if ($page->getVariable('published') !== true || $page->getVariable('exclude')) { continue; } + // $sections[section][language][] = $page $sections[$page->getSection()][$page->getVariable('language', $this->config->getLanguageDefault())][] = $page; + // nested sections + /*if ($page->getParent() !== null) { + $sections[$page->getParent()->getId()][$page->getVariable('language', $this->config->getLanguageDefault())][] = $page; + }*/ } } diff --git a/src/Step/Pages/Create.php b/src/Step/Pages/Create.php index 89f79d9c8..25ddd21e6 100644 --- a/src/Step/Pages/Create.php +++ b/src/Step/Pages/Create.php @@ -53,7 +53,6 @@ public function process(): void $total = \count($this->builder->getPagesFiles()); $count = 0; - foreach ($this->builder->getPagesFiles() as $file) { $count++; // create a page from its (Markdown) file @@ -110,7 +109,7 @@ public function process(): void $this->builder->getPages()->add($page); } - $message = sprintf('Page "%s" created', $page->getId()); + $message = sprintf('Page "%s" (%s) created', $page->getId(), $page->getType()); $this->builder->getLogger()->info($message, ['progress' => [$count, $total]]); } } diff --git a/src/Step/Pages/Load.php b/src/Step/Pages/Load.php index c23c060b4..40299d3e5 100644 --- a/src/Step/Pages/Load.php +++ b/src/Step/Pages/Load.php @@ -58,6 +58,13 @@ public function process(): void ->files() ->in($this->config->getPagesPath()) ->sort(function (SplFileInfo $a, SplFileInfo $b): int { + // root pages first + if (empty($a->getRelativePath()) && !empty($b->getRelativePath())) { + return -1; + } + if (empty($b->getRelativePath()) && !empty($a->getRelativePath())) { + return 1; + } // section's index first if ($a->getRelativePath() == $b->getRelativePath() && $a->getBasename('.' . $a->getExtension()) == 'index') { return -1; @@ -66,7 +73,7 @@ public function process(): void return 1; } // sort by name - return strnatcasecmp($a->getRealPath(), $b->getRealPath()); + return strnatcasecmp($a->getRelativePath(), $b->getRelativePath()); }); // load only one page? if ($this->page) { diff --git a/src/Step/Pages/Render.php b/src/Step/Pages/Render.php index 6d26758af..882ff29a5 100644 --- a/src/Step/Pages/Render.php +++ b/src/Step/Pages/Render.php @@ -14,8 +14,8 @@ namespace Cecil\Step\Pages; use Cecil\Builder; -use Cecil\Collection\Page\Collection; use Cecil\Collection\Page\Page; +use Cecil\Collection\Page\Type; use Cecil\Exception\RuntimeException; use Cecil\Renderer\Config; use Cecil\Renderer\Layout; @@ -63,28 +63,29 @@ public function process(): void // adds global variables $this->addGlobals(); - /** @var Collection $pages */ + // prepares pages collections $pages = $this->builder->getPages() // published only ->filter(function (Page $page) { return (bool) $page->getVariable('published'); }) - // enrichs some variables + // enriched variables ->map(function (Page $page) { $formats = $this->getOutputFormats($page); - // output formats - $page->setVariable('output', $formats); // alternates formats + $page->setVariable('output', $formats); $page->setVariable('alternates', $this->getAlternates($formats)); // translations $page->setVariable('translations', $this->getTranslations($page)); + // parent + if (($parent = $this->findParent($page)) !== null) { + $page->setParent($parent); + } return $page; }); - $total = \count($pages); - // renders each page - $count = 0; + // loads post processors $postprocessors = []; foreach ($this->config->get('output.postprocessors') as $name => $postprocessor) { if (!class_exists($postprocessor)) { @@ -94,7 +95,10 @@ public function process(): void $postprocessors[] = new $postprocessor($this->builder); $this->builder->getLogger()->debug(sprintf('Output post processor "%s" loaded', $name)); } - /** @var Page $page */ + + // renders each page + $total = \count($pages); + $count = 0; foreach ($pages as $page) { $count++; $rendered = []; @@ -107,7 +111,7 @@ public function process(): void // global site variables $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $language)); - // global config raw variables + // global config variables $this->builder->getRenderer()->addGlobal('config', new Config($this->builder, $language)); // excluded format(s)? @@ -301,4 +305,32 @@ protected function getTranslations(Page $refPage): \Cecil\Collection\Page\Collec return $pages; } + + /** + * Find page parent or null. + */ + protected function findParent(Page $page): ?Page + { + $parent = null; + $langPrefix = ''; + if ($page->getVariable('language') !== null && $page->getVariable('language') != $this->config->getLanguageDefault()) { + $langPrefix = $page->getVariable('language') . "/"; + } + // home page by default + if ($page->getType() !== Type::HOMEPAGE->value) { + $parent = $this->builder->getPages()->get($langPrefix . 'index'); + } + // recursive folder search + $folderAsArray = explode('/', (string) $page->getFolder()); + while (\count($folderAsArray) >= 1 && !empty($folderAsArray[0])) { + $parentFolder = implode('/', $folderAsArray); + if ($this->builder->getPages()->has($langPrefix . $parentFolder) && $this->builder->getPages()->get($langPrefix . $parentFolder)->getId() !== $page->getId()) { + $parent = $this->builder->getPages()->get($langPrefix . $parentFolder); + break; + } + array_pop($folderAsArray); + } + + return $parent; + } } diff --git a/tests/fixtures/website/layouts/ancestors.html.twig b/tests/fixtures/website/layouts/ancestors.html.twig new file mode 100644 index 000000000..39581cfdc --- /dev/null +++ b/tests/fixtures/website/layouts/ancestors.html.twig @@ -0,0 +1,14 @@ +{% extends ['page.html.twig', '_default/page.html.twig'] %} + +{% block content %} + +

Ancestors

+ +
+{% for item in page.ancestors|reverse %} + {% if loop.index != 1 %} > {% endif %} + {{ item.title }} +{% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/tests/fixtures/website/layouts/intl.html.twig b/tests/fixtures/website/layouts/intl.html.twig index d98fa081f..ef129db41 100644 --- a/tests/fixtures/website/layouts/intl.html.twig +++ b/tests/fixtures/website/layouts/intl.html.twig @@ -71,6 +71,8 @@
site.page('about', 'fr').title

+ {% if site.page('about', 'fr') is defined and site.page('about', 'fr') is not null %} {{ site.page('about', 'fr').title }} + {% endif %}

{% endblock content %} diff --git a/tests/fixtures/website/pages/Section/Sub section/Sub sub section/index.md b/tests/fixtures/website/pages/Section/Sub section/Sub sub section/index.md new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/website/pages/Section/Sub section/index.md b/tests/fixtures/website/pages/Section/Sub section/index.md new file mode 100644 index 000000000..fc7b05989 --- /dev/null +++ b/tests/fixtures/website/pages/Section/Sub section/index.md @@ -0,0 +1,3 @@ +--- +title: Sub section +--- diff --git a/tests/fixtures/website/pages/docs/sub-dir/Page in sub-dir.md b/tests/fixtures/website/pages/docs/sub-dir/Page in sub-dir.md new file mode 100644 index 000000000..ab893fef6 --- /dev/null +++ b/tests/fixtures/website/pages/docs/sub-dir/Page in sub-dir.md @@ -0,0 +1,3 @@ +--- +layout: nested-page +--- diff --git a/tests/fixtures/website/pages/docs/sub-section/Page in sub-section.md b/tests/fixtures/website/pages/docs/sub-section/Page in sub-section.md new file mode 100644 index 000000000..ab893fef6 --- /dev/null +++ b/tests/fixtures/website/pages/docs/sub-section/Page in sub-section.md @@ -0,0 +1,3 @@ +--- +layout: nested-page +--- diff --git a/tests/fixtures/website/pages/docs/sub-section/index.md b/tests/fixtures/website/pages/docs/sub-section/index.md new file mode 100644 index 000000000..ab893fef6 --- /dev/null +++ b/tests/fixtures/website/pages/docs/sub-section/index.md @@ -0,0 +1,3 @@ +--- +layout: nested-page +--- diff --git a/tests/fixtures/website/pages/docs/sub-section/sub-sub-section/Page in sub-sub-section.md b/tests/fixtures/website/pages/docs/sub-section/sub-sub-section/Page in sub-sub-section.md new file mode 100644 index 000000000..ab893fef6 --- /dev/null +++ b/tests/fixtures/website/pages/docs/sub-section/sub-sub-section/Page in sub-sub-section.md @@ -0,0 +1,3 @@ +--- +layout: nested-page +--- diff --git a/tests/fixtures/website/pages/docs/sub-section/sub-sub-section/index.md b/tests/fixtures/website/pages/docs/sub-section/sub-sub-section/index.md new file mode 100644 index 000000000..ab893fef6 --- /dev/null +++ b/tests/fixtures/website/pages/docs/sub-section/sub-sub-section/index.md @@ -0,0 +1,3 @@ +--- +layout: nested-page +--- diff --git a/tests/fixtures/website/themes/a-theme/layouts/index.html.twig b/tests/fixtures/website/themes/a-theme/layouts/index.html.twig index 9d03f0e81..df385860e 100644 --- a/tests/fixtures/website/themes/a-theme/layouts/index.html.twig +++ b/tests/fixtures/website/themes/a-theme/layouts/index.html.twig @@ -13,9 +13,11 @@ +{# {% if site.foo is defined %} Imported config (from theme(s)):

Variable `foo`=`{{ site.foo }}`

{% endif %} +#} {% endblock %} diff --git a/tests/fixtures/website/themes/a-theme/layouts/partials/pages_as_table.html.twig b/tests/fixtures/website/themes/a-theme/layouts/partials/pages_as_table.html.twig new file mode 100644 index 000000000..6e4984679 --- /dev/null +++ b/tests/fixtures/website/themes/a-theme/layouts/partials/pages_as_table.html.twig @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + {%- for item in site.pages ~%} + + + + + + + + + + + + + + {%- endfor ~%} +
All pages
titlefilepathidtypevirtualsectionpathfolderslugmenulang
+ {{ item.title }}
+ {{ item.date|date("d/m/Y") }}
+ {{ item.updated|date("d/m/Y") }} +
{{ item.filepath }}{{ item.id }}{{ item.type|upper }}{{ item.virtual ? 'virtual' : 'file' }}{{ item.section ?? 'root' }}{{ item.path }}{{ item.folder }}{{ item.slug }} + {%- if item.menu is defined %} + {{- dump(item.menu) ~}} + {%- endif ~%} + + {{- item.language|default ~}} +
\ No newline at end of file