diff --git a/src/Filters/GetParentDirectory.js b/src/Filters/GetParentDirectory.js new file mode 100644 index 000000000..71df3ea47 --- /dev/null +++ b/src/Filters/GetParentDirectory.js @@ -0,0 +1,49 @@ +/** + * Returns the parent directory of a given path. + * + * For directory paths (ending with /): returns the parent directory + * For file paths: returns the directory containing the file + * + * Examples: + * /mydir/ -> / + * /mydir/slug.html -> /mydir/ + * /mydir/index.md -> /mydir/ + * /a/b/c/ -> /a/b/ + * /file.html -> / + * + * @param {string} path - The path to get the parent directory of + * @returns {string} The parent directory path + */ +export default function getParentDirectory(path) { + if (!path || typeof path !== "string") { + return ""; + } + + // If path ends with /, it's a directory - get its parent + if (path.endsWith("/") && path.length > 1) { + // Remove trailing slash, then find parent + let withoutTrailing = path.slice(0, -1); + let lastSlash = withoutTrailing.lastIndexOf("/"); + if (lastSlash === -1) { + return "/"; + } + return withoutTrailing.slice(0, lastSlash + 1) || "/"; + } + + // Handle root directory + if (path === "/") { + return "/"; + } + + // Otherwise it's a file path - get the directory containing it + let lastSlash = path.lastIndexOf("/"); + if (lastSlash === -1) { + // No slash found, no parent directory + return ""; + } + if (lastSlash === 0) { + // File is at root level + return "/"; + } + return path.slice(0, lastSlash + 1); +} diff --git a/src/defaultConfigExtended.js b/src/defaultConfigExtended.js index a976f093e..a446d65d2 100644 --- a/src/defaultConfigExtended.js +++ b/src/defaultConfigExtended.js @@ -8,6 +8,7 @@ import MemoizeUtil from "./Util/MemoizeFunction.js"; import urlFilter from "./Filters/Url.js"; import getLocaleCollectionItem from "./Filters/GetLocaleCollectionItem.js"; import getCollectionItemIndex from "./Filters/GetCollectionItemIndex.js"; +import getParentDirectory from "./Filters/GetParentDirectory.js"; import { FilterPlugin as InputPathToUrlFilterPlugin } from "./Plugins/InputPathToUrl.js"; /** @@ -98,4 +99,9 @@ export default function (config) { return urlFilter.call(this, url, pathPrefix); }); + + // Returns the parent directory of a path + // For directories (/mydir/): returns parent (/mydir/ -> /) + // For files (/mydir/file.html): returns containing directory (/mydir/file.html -> /mydir/) + config.addFilter("getParentDirectory", getParentDirectory); } diff --git a/test/GetParentDirectoryTest.js b/test/GetParentDirectoryTest.js new file mode 100644 index 000000000..0d8cfad52 --- /dev/null +++ b/test/GetParentDirectoryTest.js @@ -0,0 +1,55 @@ +import test from "ava"; +import getParentDirectory from "../src/Filters/GetParentDirectory.js"; + +test("Directory paths - returns parent directory", (t) => { + // Examples from issue #3794 + t.is(getParentDirectory("/mydir/"), "/"); + + // Nested directories + t.is(getParentDirectory("/a/b/"), "/a/"); + t.is(getParentDirectory("/a/b/c/"), "/a/b/"); + t.is(getParentDirectory("/a/b/c/d/"), "/a/b/c/"); +}); + +test("File paths - returns containing directory", (t) => { + // Examples from issue #3794 + t.is(getParentDirectory("/mydir/slug.html"), "/mydir/"); + t.is(getParentDirectory("/mydir/index.md"), "/mydir/"); + + // Nested file paths + t.is(getParentDirectory("/a/b/file.html"), "/a/b/"); + t.is(getParentDirectory("/a/b/c/page.md"), "/a/b/c/"); +}); + +test("Root level paths", (t) => { + // Root directory + t.is(getParentDirectory("/"), "/"); + + // File at root + t.is(getParentDirectory("/file.html"), "/"); + t.is(getParentDirectory("/index.md"), "/"); +}); + +test("Edge cases", (t) => { + // Empty and invalid inputs + t.is(getParentDirectory(""), ""); + t.is(getParentDirectory(null), ""); + t.is(getParentDirectory(undefined), ""); + + // Non-string inputs + t.is(getParentDirectory(123), ""); + t.is(getParentDirectory({}), ""); + + // Relative paths (no leading slash) + t.is(getParentDirectory("file.html"), ""); + t.is(getParentDirectory("dir/file.html"), "dir/"); + t.is(getParentDirectory("a/b/c/"), "a/b/"); +}); + +test("Single segment paths", (t) => { + // Directory with single segment + t.is(getParentDirectory("/foo/"), "/"); + + // File with single segment at root + t.is(getParentDirectory("/foo"), "/"); +});