diff --git a/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java b/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java index 2d65c7123..ee52a43fe 100644 --- a/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java +++ b/znai-cli/src/main/java/org/testingisdocumenting/znai/cli/ZnaiCliApp.java @@ -179,6 +179,7 @@ private WebSite generateDocs(Path sourceRoot) { withAdditionalLookupPaths(config.getLookupPaths()). withFooterPath(sourceRoot.resolve("footer.md")). withExtensionsDefPath(sourceRoot.resolve("extensions.json")). + withRedirectsPath(sourceRoot.resolve("page-redirects.csv")). withGlobalReferencesPathNoExt(sourceRoot.resolve("references")). withGlobalPluginParamsPath(sourceRoot.resolve(PLUGIN_PARAMS_FILE_NAME)). withWebResources(favIconResource). diff --git a/znai-docs/znai/release-notes/1.81/add-2025-10-19-page-redirects.md b/znai-docs/znai/release-notes/1.81/add-2025-10-19-page-redirects.md new file mode 100644 index 000000000..fa2189c00 --- /dev/null +++ b/znai-docs/znai/release-notes/1.81/add-2025-10-19-page-redirects.md @@ -0,0 +1 @@ +* Add: `page-redirects.csv` support to help with page renames \ No newline at end of file diff --git a/znai-tests/src/test/groovy/pages/PreviewServer.groovy b/znai-tests/src/test/groovy/pages/PreviewServer.groovy index a441f53da..ec2decad3 100644 --- a/znai-tests/src/test/groovy/pages/PreviewServer.groovy +++ b/znai-tests/src/test/groovy/pages/PreviewServer.groovy @@ -22,4 +22,8 @@ class PreviewServer { void openPreview(port) { browser.open("http://localhost:${port}/preview") } + + void openPreviewWithUrl(port, url) { + browser.open("http://localhost:${port}/preview/${url}") + } } diff --git a/znai-tests/src/test/groovy/sampledoc/page-redirects.csv b/znai-tests/src/test/groovy/sampledoc/page-redirects.csv new file mode 100644 index 000000000..e0ac9f18f --- /dev/null +++ b/znai-tests/src/test/groovy/sampledoc/page-redirects.csv @@ -0,0 +1 @@ +chapter-three/page-one,chapter-one/links \ No newline at end of file diff --git a/znai-tests/src/test/groovy/scenarios/sampleDoc.groovy b/znai-tests/src/test/groovy/scenarios/sampleDoc.groovy index 684672979..4ae784ead 100644 --- a/znai-tests/src/test/groovy/scenarios/sampleDoc.groovy +++ b/znai-tests/src/test/groovy/scenarios/sampleDoc.groovy @@ -40,6 +40,11 @@ scenario("validate page outside of chapter") { docContent.paragraphs.get("Files don't have to belong to chapters if you have simple docs").waitTo visible } +scenario("check redirect page") { + previewServer.openPreviewWithUrl(port, "chapter-three/page-one") + docContent.title.waitToBe == "Links" +} + scenario("validate uploads files") { def baseUrl = "http://localhost:$port/preview" diff --git a/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/PageRedirects.java b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/PageRedirects.java new file mode 100644 index 000000000..d07ff0e64 --- /dev/null +++ b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/PageRedirects.java @@ -0,0 +1,87 @@ +package org.testingisdocumenting.znai.website; + +import org.testingisdocumenting.znai.html.Deployer; +import org.testingisdocumenting.znai.parser.table.CsvTableParser; +import org.testingisdocumenting.znai.parser.table.MarkupTableData; +import org.testingisdocumenting.znai.structure.DocStructure; +import org.testingisdocumenting.znai.structure.TocItem; +import org.testingisdocumenting.znai.utils.FileUtils; +import org.testingisdocumenting.znai.utils.ResourceUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + + +public class PageRedirects { + private final Deployer deployer; + private final Path csvPath; + private final DocStructure docStructure; + + public record FromTo (String oldLink, String newDirName, String newFileNameWithoutExtension) {} + + public PageRedirects(DocStructure docStructure, Deployer deployer, Path csvPath) { + this.docStructure = docStructure; + this.deployer = deployer; + this.csvPath = csvPath; + } + + public boolean isPresent() { + return Files.exists(csvPath); + } + + public void deployRedirectPages() { + List redirects = parse(csvPath); + redirects.forEach(this::deployRedirect); + } + + private void deployRedirect(FromTo fromTo) { + TocItem tocItem = docStructure.tableOfContents().findTocItem(fromTo.newDirName, fromTo.newFileNameWithoutExtension); + if (tocItem == null) { + throw new RuntimeException("toc item not found: " + + fromTo.newDirName + "/" + fromTo.newFileNameWithoutExtension); + } + + String redirectUrl = docStructure.fullUrl( + tocItem.getDirName() + "/" + tocItem.getFileNameWithoutExtension()); + String redirectPage = ResourceUtils.textContent("template/redirect.html") + .replace("${newUrl}", redirectUrl); + deployer.deploy(fromTo.oldLink + "/index.html", redirectPage); + } + + private static List parse(Path csvPath) { + return parse(FileUtils.fileTextContent(csvPath)); + } + + protected static List parse(String content) { + String withoutComments = Arrays.stream(content.split("\n")) + .filter(line -> !line.startsWith("#")) + .collect(Collectors.joining("\n")); + + MarkupTableData tableData = CsvTableParser.parseWithHeader(withoutComments, "reference", "url"); + + List result = new ArrayList<>(); + tableData.forEachRow(row -> { + String newUrl = row.get(1).toString(); + String[] parts = newUrl.split("/"); + String newDirName; + String newFileNameWithoutExtension; + if (parts.length == 1) { + newDirName = ""; + newFileNameWithoutExtension = parts[0]; + } else if (parts.length == 2) { + newDirName = parts[0]; + newFileNameWithoutExtension = parts[1]; + } else { + throw new RuntimeException("invalid url format, expected [dirName/]fileName"); + } + + result.add(new FromTo(row.get(0), newDirName, newFileNameWithoutExtension)); + }); + + return result; + } +} diff --git a/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/WebSite.java b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/WebSite.java index 37ec759fb..cfaeacab6 100644 --- a/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/WebSite.java +++ b/znai-website-gen/src/main/java/org/testingisdocumenting/znai/website/WebSite.java @@ -202,6 +202,7 @@ public void deploy() { reportPhase("deploying documentation"); generatePages(); generateChapterIndexRedirectPages(); + generatePageRedirects(); generateSearchIndex(); generateLlmContent(); deployToc(); @@ -652,6 +653,16 @@ private void generateChapterIndexRedirectPages() { }); } + private void generatePageRedirects() { + PageRedirects pageRedirects = new PageRedirects(docStructure, deployer, cfg.redirectsPaths); + if (!pageRedirects.isPresent()) { + return; + } + + reportPhase("generating page redirects"); + pageRedirects.deployRedirectPages(); + } + private void generateSearchIndex() { reportPhase("generating search index"); @@ -940,6 +951,7 @@ public static class Configuration { private Path docRootPath; private Path footerPath; private Path extensionsDefPath; + private Path redirectsPaths; private Path globalReferencesPathNoExt; private Path pluginParamsPath; private final List webResources; @@ -980,6 +992,11 @@ public Configuration withExtensionsDefPath(Path path) { return this; } + public Configuration withRedirectsPath(Path path) { + redirectsPaths = path.toAbsolutePath(); + return this; + } + public Configuration withGlobalReferencesPathNoExt(Path path) { globalReferencesPathNoExt = path.toAbsolutePath(); return this; diff --git a/znai-website-gen/src/test/groovy/org/testingisdocumenting/znai/website/PageRedirectsTest.groovy b/znai-website-gen/src/test/groovy/org/testingisdocumenting/znai/website/PageRedirectsTest.groovy new file mode 100644 index 000000000..727951737 --- /dev/null +++ b/znai-website-gen/src/test/groovy/org/testingisdocumenting/znai/website/PageRedirectsTest.groovy @@ -0,0 +1,27 @@ +package org.testingisdocumenting.znai.website + +import org.junit.Test + +import static org.testingisdocumenting.webtau.WebTauCore.* + +class PageRedirectsTest { + @Test + void "parse redirects"() { + def result = PageRedirects.parse("""# optional comment +old-chapter/old-page,new-chapter/new-page +old-chapter/old-page-two,top-level-new-page +""") + result.should == [ "oldLink" | "newDirName" | "newFileNameWithoutExtension"] { + __________________________________________________________________________ + "old-chapter/old-page" | "new-chapter" | "new-page" + "old-chapter/old-page-two" | "" | "top-level-new-page" } + } + + @Test + void "validation checks"() { + code { + PageRedirects.parse("""# optional comment +old-chapter/old-page,new-chapter/new-page/sub-page +""") } should throwException("invalid url format, expected [dirName/]fileName") + } +}