From eafdeaff951f7b958078cec64e853b64a8614744 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Nov 2025 04:20:30 +0000 Subject: [PATCH] feat: Add plugin to remap README pages to index URLs - Implement a new `jekyll-readme-index` plugin. - This plugin intercepts `README` files during page initialization. - It calculates a new URL for `README` pages, mapping them to their parent directory's index URL (e.g., `/README.html` becomes `/`, and `/foo/README.html` becomes `/foo/`). - The URL remapping is achieved by setting the `permalink` field in the page's front matter. - This approach leverages Jekyll's standard way of handling permalinks. - The plugin includes helper functions `isReadmePage` and `calculateIndexURL` for logic. - Thorough unit tests are provided for both the helper functions and the plugin's `PostInitPage` hook. --- plugins/readme_index.go | 57 +++++++++++++++ plugins/readme_index_test.go | 130 +++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 plugins/readme_index.go create mode 100644 plugins/readme_index_test.go diff --git a/plugins/readme_index.go b/plugins/readme_index.go new file mode 100644 index 0000000..f7ffdce --- /dev/null +++ b/plugins/readme_index.go @@ -0,0 +1,57 @@ +package plugins + +import ( + "path" + "path/filepath" + "strings" +) + +type jekyllReadmeIndexPlugin struct{ plugin } + +func init() { + register("jekyll-readme-index", jekyllReadmeIndexPlugin{}) +} + +func (p jekyllReadmeIndexPlugin) PostInitPage(s Site, page Page) error { + if isReadmePage(page) { + // Calculate the new URL for this README page + oldURL := page.URL() + newURL := calculateIndexURL(oldURL) + + // Set the permalink in frontmatter to change the URL + if newURL != oldURL { + fm := page.FrontMatter() + fm["permalink"] = newURL + } + } + return nil +} + +// isReadmePage checks if a page is a README file +func isReadmePage(page Page) bool { + source := page.Source() + if source == "" { + return false + } + basename := filepath.Base(source) + // Check for README.md, README.markdown, README.mdown, etc. + nameWithoutExt := strings.TrimSuffix(basename, filepath.Ext(basename)) + return strings.EqualFold(nameWithoutExt, "README") +} + +// calculateIndexURL converts a README URL to its index URL +// e.g., "/README.html" -> "/" +// +// "/foo/README.html" -> "/foo/" +// "/foo/bar/README.html" -> "/foo/bar/" +func calculateIndexURL(url string) string { + dir := path.Dir(url) + if dir == "." || dir == "" { + return "/" + } + // Ensure the directory path ends with a slash + if !strings.HasSuffix(dir, "/") { + dir += "/" + } + return dir +} diff --git a/plugins/readme_index_test.go b/plugins/readme_index_test.go new file mode 100644 index 0000000..5ca77bc --- /dev/null +++ b/plugins/readme_index_test.go @@ -0,0 +1,130 @@ +package plugins + +import ( + "testing" + + "github.com/osteele/gojekyll/pages" + "github.com/stretchr/testify/require" +) + +func TestIsReadmePage(t *testing.T) { + tests := []struct { + name string + source string + expected bool + }{ + {"root README.md", "/path/to/site/README.md", true}, + {"root README.markdown", "/path/to/site/README.markdown", true}, + {"nested README.md", "/path/to/site/foo/README.md", true}, + {"nested README.markdown", "/path/to/site/foo/bar/README.markdown", true}, + {"case insensitive", "/path/to/site/readme.md", true}, + {"not a README", "/path/to/site/index.md", false}, + {"empty source", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock page with source in frontmatter + fm := make(pages.FrontMatter) + page := &mockPage{fm: fm} + // Override the Source method by creating a custom page wrapper + testPage := &testPageWithSource{ + Page: page, + source: tt.source, + } + result := isReadmePage(testPage) + require.Equal(t, tt.expected, result) + }) + } +} + +// testPageWithSource wraps mockPage to provide a custom Source method +type testPageWithSource struct { + Page + source string +} + +func (p *testPageWithSource) Source() string { + return p.source +} + +func TestCalculateIndexURL(t *testing.T) { + tests := []struct { + name string + url string + expected string + }{ + {"root README", "/README.html", "/"}, + {"one level deep", "/foo/README.html", "/foo/"}, + {"two levels deep", "/foo/bar/README.html", "/foo/bar/"}, + {"already has slash", "/foo/", "/foo/"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateIndexURL(tt.url) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestReadmeIndexPlugin_PostInitPage(t *testing.T) { + plugin := jekyllReadmeIndexPlugin{} + site := &mockSite{} + + tests := []struct { + name string + source string + url string + expectedChanged bool + expectedURL string + }{ + { + name: "root README", + source: "/path/to/site/README.md", + url: "/README.html", + expectedChanged: true, + expectedURL: "/", + }, + { + name: "nested README", + source: "/path/to/site/foo/README.md", + url: "/foo/README.html", + expectedChanged: true, + expectedURL: "/foo/", + }, + { + name: "non-README page", + source: "/path/to/site/index.md", + url: "/index.html", + expectedChanged: false, + expectedURL: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fm := make(pages.FrontMatter) + page := &mockPage{ + url: tt.url, + fm: fm, + } + testPage := &testPageWithSource{ + Page: page, + source: tt.source, + } + + err := plugin.PostInitPage(site, testPage) + require.NoError(t, err) + + if tt.expectedChanged { + permalink, hasPermalink := fm["permalink"] + require.True(t, hasPermalink, "permalink should be set in frontmatter") + require.Equal(t, tt.expectedURL, permalink) + } else { + _, hasPermalink := fm["permalink"] + require.False(t, hasPermalink, "permalink should not be set for non-README pages") + } + }) + } +}