From 78a5bf36732f443184e3922abe37575ab45c3a32 Mon Sep 17 00:00:00 2001 From: Romain Manni-Bucau Date: Fri, 16 Feb 2024 20:50:08 +0100 Subject: [PATCH] [minisite] enable to configure prev/next links from the page attributes --- .../src/main/minisite/content/mojos.adoc | 2 + .../versioning/VersioningInjectorTest.java | 1 + .../io/yupiik/tools/minisite/MiniSite.java | 69 ++++++++++++++++--- .../minisite/assets/css/theme.css | 42 ++++++++++- .../minisite/page-content.html | 1 + .../minisite/page-footer-nav-link.html | 4 ++ .../minisite/page-footer-nav.html | 4 ++ .../yupiik/tools/minisite/MiniSiteTest.java | 16 +++++ .../MiniSiteTest/footerNav/content/page.adoc | 7 ++ .../command/VersionInjectorCommandTest.java | 1 + 10 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav-link.html create mode 100644 minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav.html create mode 100644 minisite-core/src/test/resources/sites/MiniSiteTest/footerNav/content/page.adoc diff --git a/_documentation/src/main/minisite/content/mojos.adoc b/_documentation/src/main/minisite/content/mojos.adoc index 2fd29ef4..29457197 100644 --- a/_documentation/src/main/minisite/content/mojos.adoc +++ b/_documentation/src/main/minisite/content/mojos.adoc @@ -295,6 +295,8 @@ Some specific attributes enables to customize the generation. Here is their list * `minisite-skip=[true|false]` enables to skip a `.adoc` rendering even if not in `_partials` directory. * `minisite-path=` enables to force the relative path of the file, for example a file name foo-bar.adoc with the attribute `minisite-path` set to `foo/bar.html` will output a `foo/bar.html` file instead of `foo-bar.html`. Note however it does not rewrite the links to ensure to use `link:.....html[]` instead of `ref` to link this page then. * `minisite-highlightjs-skip` enables to not setup highlight.js for the page (useful with swagger-ui for example). +* `minisite-nav-prev-label`/`minisite-nav-next-label` enables to add a bottom page "previous"/"next" link to another page, this attribute defines its label. +* `minisite-nav-prev-link`/`minisite-nav-next-link` enables to add a bottom page "previous"/"next" link to another page, this attribute defines its link (label is required), if label is defined but not the link, the link is the label lowercased with iphens instead of spaces and html extension. === Index generation diff --git a/html-versioning-injector/src/test/java/io/yupiik/tools/injector/versioning/VersioningInjectorTest.java b/html-versioning-injector/src/test/java/io/yupiik/tools/injector/versioning/VersioningInjectorTest.java index ce03f222..dc37f011 100644 --- a/html-versioning-injector/src/test/java/io/yupiik/tools/injector/versioning/VersioningInjectorTest.java +++ b/html-versioning-injector/src/test/java/io/yupiik/tools/injector/versioning/VersioningInjectorTest.java @@ -135,6 +135,7 @@ void inject(@TempDir final Path workDir) throws IOException { "

Content.

\n" + "\n" + " \n" + + " \n" + " \n" + " ", extractContent(Files.readString(output.resolve("page.html"))).strip().replace("\r", "")); diff --git a/minisite-core/src/main/java/io/yupiik/tools/minisite/MiniSite.java b/minisite-core/src/main/java/io/yupiik/tools/minisite/MiniSite.java index 38227300..f22cfe21 100644 --- a/minisite-core/src/main/java/io/yupiik/tools/minisite/MiniSite.java +++ b/minisite-core/src/main/java/io/yupiik/tools/minisite/MiniSite.java @@ -64,6 +64,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; +import static java.util.Locale.ROOT; import static java.util.Map.entry; import static java.util.Objects.requireNonNull; import static java.util.Optional.of; @@ -94,6 +95,7 @@ public void run() { configuration.getAsciidoctorConfiguration().info().accept("Rendering (and upload) skipped"); return; } + final Object options = createOptions(); configuration.getAsciidoc().withInstance(configuration.getAsciidoctorConfiguration(), a -> { executeInMinisiteClassLoader(() -> doRender(a, options)); @@ -141,7 +143,8 @@ protected Consumer> render(final Page page, final Path ht final Asciidoc.AsciidocInstance asciidoctor, final Object options, final boolean withLeftMenuIfConfigured, final Function postProcessor, - final Function customInterpolations) { + final Function customInterpolations, + final Function footerNavTemplate) { final Path templates = getTemplatesDir(); final String titleTemplate = findPageTemplate(templates, "page-title"); final String contentTemplate = findPageTemplate(templates, "page-content"); @@ -161,7 +164,14 @@ protected Consumer> render(final Page page, final Path ht if ("title".equals(key)) { return title; } - return getDefaultInterpolation(key, page, asciidoctor, options, customInterpolations); + return getDefaultInterpolation(key, page, asciidoctor, options, k -> { + switch (k) { + case "pageFooterNav": + return footerNavTemplate.apply(page); + default: + return getDefaultInterpolation(k, page, asciidoctor, options, customInterpolations); + } + }); }).replace(contentTemplate); final String content = template.apply(new Page( '/' + configuration.getTarget().relativize(html).toString().replace(File.separatorChar, '/'), @@ -543,7 +553,8 @@ public void doRender(final Asciidoc.AsciidocInstance asciidoctor, final Object o if (Files.exists(content)) { final List blog = new ArrayList<>(); final List>> pageToRender = new ArrayList>>(); - pages.forEach(page -> pageToRender.add(onVisitedFile(page, asciidoctor, options, files, now, blog))); + final Function footerNavTemplate = loadNavTemplates(); + pages.forEach(page -> pageToRender.add(onVisitedFile(page, asciidoctor, options, files, now, blog, footerNavTemplate))); hasBlog = (!blog.isEmpty() && configuration.isGenerateBlog()); template = createTemplate(options, asciidoctor, hasBlog); final Function tpl = template; @@ -749,7 +760,8 @@ protected String xmlEscape(final String text) { } protected Consumer> onVisitedFile(final Page page, final Asciidoc.AsciidocInstance asciidoctor, final Object options, - final Map files, final OffsetDateTime now, final List blog) { + final Map files, final OffsetDateTime now, final List blog, + final Function footerNavTemplate) { if (page.attributes.containsKey("minisite-skip")) { return t -> { }; @@ -773,7 +785,7 @@ protected Consumer> onVisitedFile(final Page page, final }; } else { return template -> { - render(page, out, asciidoctor, options, true, identity(), null).accept(template); + render(page, out, asciidoctor, options, true, identity(), null, footerNavTemplate).accept(template); configuration.getAsciidoctorConfiguration().debug().accept("Rendered " + page.relativePath + " to " + out); }; } @@ -859,7 +871,7 @@ protected List generateBlog(final List blog, final Asciidoc.As default: return null; } - }).accept(template); + }, p -> "").accept(template); configuration.getAsciidoctorConfiguration().debug().accept("Rendered " + bp.page.relativePath + " to " + out); }); return allCategories; @@ -976,7 +988,7 @@ protected Collection paginatePer(final String singular, final String plu .collect(joining("\n"))), baseBlog.resolve(singular + "/index.html"), asciidoctor, options, false, - this::markPageAsBlog, null).accept(template); + this::markPageAsBlog, null, p -> "").accept(template); return perCriteria.keySet(); } @@ -1051,7 +1063,7 @@ protected void paginateBlogPages(final BiFunction pref } }).replace(contentTemplate)), output, asciidoctor, options, false, - this::markPageAsBlog, null).accept(template); + this::markPageAsBlog, null, p -> "").accept(template); }); final Path indexRedirect = baseBlog.resolve(pageRelativeFolder + "index.html"); @@ -1082,6 +1094,27 @@ protected boolean hasSearch() { return !"none".equals(configuration.getSearchIndexName()) && configuration.getSearchIndexName() != null; } + protected Function loadNavTemplates() { + final Path templatesDir = getTemplatesDir(); + final String globalTemplate = readTemplates(templatesDir, List.of("page-footer-nav.html")); + final String linkTemplate = readTemplates(templatesDir, List.of("page-footer-nav-link.html")); + return page -> { + if (page.attributes == null) { + return ""; + } + + final NavLink prev = new NavLink(page.attributes, "minisite-nav-prev-"); + final NavLink next = new NavLink(page.attributes, "minisite-nav-next-"); + if (prev.link == null && next.link == null) { + return ""; + } + + return globalTemplate + .replace("{{previousLink}}", prev.render(linkTemplate, "prev", "Previous")) + .replace("{{nextLink}}", next.render(linkTemplate, "next", "Next")); + }; + } + public Function createTemplate(final Object options, final Asciidoc.AsciidocInstance asciidoctor, final boolean hasBlog) { @@ -1350,4 +1383,24 @@ public static class Page { private final Map attributes; private final String content; } + + private static class NavLink { + private final String label; + private final String link; + + private NavLink(final Map props, final String prefix) { + this.label = props.get(prefix + "label"); + this.link = ofNullable(props.get(prefix + "link")) + .filter(Predicate.not(String::isBlank)) + .orElseGet(() -> label == null || label.isBlank() ? null : label.toLowerCase(ROOT).replace(' ', '-') + ".html"); + } + + public String render(final String template, final String clazz, final String subLabel) { + return link == null || link.isBlank() ? "" : template + .replace("{{class}}", "page-footer-nav-link-" + clazz) + .replace("{{link}}", link) + .replace("{{subLabel}}", subLabel) + .replace("{{label}}", label); + } + } } diff --git a/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/assets/css/theme.css b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/assets/css/theme.css index a0e6ad57..d7a2da1c 100644 --- a/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/assets/css/theme.css +++ b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/assets/css/theme.css @@ -412,4 +412,44 @@ div.colist.arabic > ol > li > p { right: unset; left: 1rem; top: 0.2rem; -} \ No newline at end of file +} + +/* for page-footer-nav templates */ +.page-footer-nav { + grid-gap: 1rem; + gap: 1rem; + margin-top: 3rem; + display: grid; + grid-template-columns: repeat(2,1fr); +} +.page-footer-nav > a { + border: 1px solid lightgrey; + border-radius: .4rem; + display: block; + height: 100%; + line-height: 1.65; + padding: 1rem; + transition: border-color 200ms cubic-bezier(0.08,0.52,0.52,1); + text-decoration: none; +} +.page-footer-nav > a > div:first-child { + color: grey; + font-size: .875rem; + font-weight: 500; + margin-bottom: 0.25rem; +} +.page-footer-nav > a > div:last-child { + font-size: 1rem; + font-weight: 700; + word-break: break-word; +} +.page-footer-nav > a.page-footer-nav-link-next { + grid-column: 2/3; + text-align: right; +} +.page-footer-nav > a.page-footer-nav-link-prev > div:last-child:before { + content: "« "; +} +.page-footer-nav > a.page-footer-nav-link-next > div:last-child:after { + content: " »"; +} diff --git a/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-content.html b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-content.html index 78eda058..951e6d4b 100644 --- a/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-content.html +++ b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-content.html @@ -3,5 +3,6 @@
{{{body}}} {{{defaultEndOfContent?emptyIfMissing=true}}} + {{{pageFooterNav?emptyIfMissing=true}}}
\ No newline at end of file diff --git a/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav-link.html b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav-link.html new file mode 100644 index 00000000..cbab2f6d --- /dev/null +++ b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav-link.html @@ -0,0 +1,4 @@ + +
{{subLabel}}
+
{{label}}
+
\ No newline at end of file diff --git a/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav.html b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav.html new file mode 100644 index 00000000..dbb873ad --- /dev/null +++ b/minisite-core/src/main/resources/yupiik-tools-maven-plugin/minisite/page-footer-nav.html @@ -0,0 +1,4 @@ + diff --git a/minisite-core/src/test/java/io/yupiik/tools/minisite/MiniSiteTest.java b/minisite-core/src/test/java/io/yupiik/tools/minisite/MiniSiteTest.java index f9bcd71f..22572d75 100644 --- a/minisite-core/src/test/java/io/yupiik/tools/minisite/MiniSiteTest.java +++ b/minisite-core/src/test/java/io/yupiik/tools/minisite/MiniSiteTest.java @@ -27,6 +27,22 @@ @MiniSiteConfigurationBuilderProvider class MiniSiteTest { + @Test + void footerNav(final MiniSiteConfigurationBuilderProvider.Asserts asserts, + final MiniSiteConfiguration.MiniSiteConfigurationBuilder builder) { + new MiniSite(builder.build()).run(); + asserts.assertContains("page.html", " "); + } + @Test void customTemplates(final MiniSiteConfigurationBuilderProvider.Asserts asserts, final MiniSiteConfiguration.MiniSiteConfigurationBuilder builder) { diff --git a/minisite-core/src/test/resources/sites/MiniSiteTest/footerNav/content/page.adoc b/minisite-core/src/test/resources/sites/MiniSiteTest/footerNav/content/page.adoc new file mode 100644 index 00000000..230ac67e --- /dev/null +++ b/minisite-core/src/test/resources/sites/MiniSiteTest/footerNav/content/page.adoc @@ -0,0 +1,7 @@ += Page 1 +:minisite-nav-prev-label: Introduction +// :minisite-nav-prev-link: introduction.html +:minisite-nav-next-label: Getting Started +// :minisite-nav-next-link: getting-started.html + +Content. diff --git a/yupiik-tools-cli/src/test/java/io/yupiik/tools/cli/command/VersionInjectorCommandTest.java b/yupiik-tools-cli/src/test/java/io/yupiik/tools/cli/command/VersionInjectorCommandTest.java index ac6440ed..d8d0217a 100644 --- a/yupiik-tools-cli/src/test/java/io/yupiik/tools/cli/command/VersionInjectorCommandTest.java +++ b/yupiik-tools-cli/src/test/java/io/yupiik/tools/cli/command/VersionInjectorCommandTest.java @@ -116,6 +116,7 @@ void run(@TempDir final Path workDir) throws IOException { "

Content.

\n" + "\n" + " \n" + + " \n" + " \n" + " ", extractContent(Files.readString(output.resolve("page.html"))).strip());