Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions tools/client_wiki.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,49 @@ package tools

import (
"fmt"
"net/url"
"strings"

"github.com/raohwork/forgejo-mcp/types"
)

// wikiPageNameToSlug converts a wiki page title to the URL slug format
// expected by the Gitea API. Gitea's wiki API endpoints require the
// "sub_url" slug rather than the display title. The slug format:
// - Spaces are replaced with hyphens
// - Forward slashes are URL-encoded as %2F
// - Pages with slashes get a ".-" suffix appended
// - If the input already looks like a sub_url (contains %2F), it is
// returned as-is to avoid double-encoding
func wikiPageNameToSlug(pageName string) string {
// If it already contains %2F, assume it's already a sub_url slug
if strings.Contains(pageName, "%2F") || strings.Contains(pageName, "%2f") {
return pageName
}

// Replace spaces with hyphens (Gitea wiki convention)
slug := strings.ReplaceAll(pageName, " ", "-")

// If no slashes, return as-is (flat page like "Home")
if !strings.Contains(slug, "/") {
return url.PathEscape(slug)
}

// URL-encode each path segment and join with %2F
parts := strings.Split(slug, "/")
for i, part := range parts {
parts[i] = url.PathEscape(part)
}
result := strings.Join(parts, "%2F")

// Gitea appends ".-" to slugs for pages with path separators
if !strings.HasSuffix(result, ".-") {
result += ".-"
}

return result
}

// MyListWikiPages lists all wiki pages in a repository.
// GET /repos/{owner}/{repo}/wiki/pages
func (c *Client) MyListWikiPages(owner, repo string) ([]*types.MyWikiPageMetaData, error) {
Expand All @@ -27,9 +66,11 @@ func (c *Client) MyListWikiPages(owner, repo string) ([]*types.MyWikiPageMetaDat
}

// MyGetWikiPage gets a single wiki page by name.
// The pageName can be either the display title (e.g. "architecture/overview")
// or the sub_url slug — it will be converted automatically.
// GET /repos/{owner}/{repo}/wiki/page/{pageName}
func (c *Client) MyGetWikiPage(owner, repo, pageName string) (*types.MyWikiPage, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, wikiPageNameToSlug(pageName))

var result types.MyWikiPage
err := c.sendSimpleRequest("GET", endpoint, nil, &result)
Expand All @@ -55,9 +96,10 @@ func (c *Client) MyCreateWikiPage(owner, repo string, options types.MyCreateWiki
}

// MyDeleteWikiPage deletes a wiki page.
// The pageName can be either the display title or the sub_url slug.
// DELETE /repos/{owner}/{repo}/wiki/page/{pageName}
func (c *Client) MyDeleteWikiPage(owner, repo, pageName string) error {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, wikiPageNameToSlug(pageName))

// DELETE returns 204 No Content on success
var result interface{}
Expand All @@ -70,9 +112,10 @@ func (c *Client) MyDeleteWikiPage(owner, repo, pageName string) error {
}

// MyEditWikiPage edits an existing wiki page.
// The pageName can be either the display title or the sub_url slug.
// PATCH /repos/{owner}/{repo}/wiki/page/{pageName}
func (c *Client) MyEditWikiPage(owner, repo, pageName string, options types.MyCreateWikiPageOptions) (*types.MyWikiPage, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, wikiPageNameToSlug(pageName))

var result types.MyWikiPage
err := c.sendSimpleRequest("PATCH", endpoint, options, &result)
Expand Down
60 changes: 60 additions & 0 deletions tools/client_wiki_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package tools

import "testing"

func TestWikiPageNameToSlug(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "flat page no conversion needed",
input: "Home",
expected: "Home",
},
{
name: "flat page with spaces",
input: "Getting Started",
expected: "Getting-Started",
},
{
name: "single level nested page",
input: "architecture/overview",
expected: "architecture%2Foverview.-",
},
{
name: "deep nested page",
input: "architecture/mflow/MFLOW-glossary",
expected: "architecture%2Fmflow%2FMFLOW-glossary.-",
},
{
name: "nested page with spaces",
input: "getting started/quick reference",
expected: "getting-started%2Fquick-reference.-",
},
{
name: "already encoded sub_url passthrough",
input: "architecture%2Foverview.-",
expected: "architecture%2Foverview.-",
},
{
name: "already encoded lowercase passthrough",
input: "architecture%2foverview.-",
expected: "architecture%2foverview.-",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := wikiPageNameToSlug(tt.input)
if got != tt.expected {
t.Errorf("wikiPageNameToSlug(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}