Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

173 lines
5.1 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package wiki
import (
"net/url"
"path"
"strings"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/modules/git"
api "github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/modules/util"
"github.com/hanzoai/git/services/convert"
)
// To define the wiki related concepts:
// * Display Segment: the text what user see for a wiki page (aka, the title):
// - "Home Page"
// - "100% Free"
// - "2000-01-02 meeting"
// * Web Path:
// - "/wiki/Home-Page"
// - "/wiki/100%25+Free"
// - "/wiki/2000-01-02+meeting.-"
// - If a segment has a suffix "DashMarker(.-)", it means that there is no dash-space conversion for this segment.
// - If a WebPath is a "*.md" pattern, then use the unescaped value directly as GitPath, to make users can access the raw file.
// * Git Path (only space doesn't need to be escaped):
// - "/.wiki.git/Home-Page.md"
// - "/.wiki.git/100%25 Free.md"
// - "/.wiki.git/2000-01-02 meeting.-.md"
// TODO: support subdirectory in the future
//
// Although this package now has the ability to support subdirectory, but the route package doesn't:
// * Double-escaping problem: the URL "/wiki/abc%2Fdef" becomes "/wiki/abc/def" by ctx.PathParam, which is incorrect
// * This problem should have been 99% fixed, but it needs more tests.
// * The old wiki code's behavior is always using %2F, instead of subdirectory, so there are a lot of legacy "%2F" files in user wikis.
type WebPath string
var reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
func validateWebPath(name WebPath) error {
for _, s := range WebPathSegments(name) {
if util.SliceContainsString(reservedWikiNames, s) {
return repo_model.ErrWikiReservedName{Title: s}
}
}
return nil
}
func hasDashMarker(s string) bool {
return strings.HasSuffix(s, ".-")
}
func removeDashMarker(s string) string {
return strings.TrimSuffix(s, ".-")
}
func addDashMarker(s string) string {
return s + ".-"
}
func unescapeSegment(s string) (string, error) {
if hasDashMarker(s) {
s = removeDashMarker(s)
} else {
s = strings.ReplaceAll(s, "-", " ")
}
unescaped, err := url.QueryUnescape(s)
if err != nil {
return s, err // un-escaping failed, but it's still safe to return the original string, because it is only a title for end users
}
return unescaped, nil
}
func escapeSegToWeb(s string, hadDashMarker bool) string {
if hadDashMarker || strings.Contains(s, "-") || strings.HasSuffix(s, ".md") {
s = addDashMarker(s)
} else {
s = strings.ReplaceAll(s, " ", "-")
}
s = url.QueryEscape(s)
return s
}
func WebPathSegments(s WebPath) []string {
a := strings.Split(string(s), "/")
for i := range a {
a[i], _ = unescapeSegment(a[i])
}
return a
}
func WebPathToGitPath(s WebPath) string {
if strings.HasSuffix(string(s), ".md") {
ret, _ := url.PathUnescape(string(s))
return util.PathJoinRelX(ret)
}
a := strings.Split(string(s), "/")
for i := range a {
shouldAddDashMarker := hasDashMarker(a[i])
a[i], _ = unescapeSegment(a[i])
a[i] = escapeSegToWeb(a[i], shouldAddDashMarker)
a[i] = strings.ReplaceAll(a[i], "%20", " ") // space is safe to be kept in git path
a[i] = strings.ReplaceAll(a[i], "+", " ")
}
return strings.Join(a, "/") + ".md"
}
func GitPathToWebPath(s string) (wp WebPath, err error) {
if !strings.HasSuffix(s, ".md") {
return "", repo_model.ErrWikiInvalidFileName{FileName: s}
}
s = strings.TrimSuffix(s, ".md")
a := strings.Split(s, "/")
for i := range a {
shouldAddDashMarker := hasDashMarker(a[i])
if a[i], err = unescapeSegment(a[i]); err != nil {
return "", err
}
a[i] = escapeSegToWeb(a[i], shouldAddDashMarker)
}
return WebPath(strings.Join(a, "/")), nil
}
func WebPathToUserTitle(s WebPath) (dir, display string) {
dir = path.Dir(string(s))
display = path.Base(string(s))
if before, ok := strings.CutSuffix(display, ".md"); ok {
display = before
display, _ = url.PathUnescape(display)
}
display, _ = unescapeSegment(display)
return dir, display
}
func WebPathToURLPath(s WebPath) string {
return string(s)
}
func WebPathFromRequest(s string) WebPath {
s = util.PathJoinRelX(s)
// The old wiki code's behavior is always using %2F, instead of subdirectory.
s = strings.ReplaceAll(s, "/", "%2F")
return WebPath(s)
}
func UserTitleToWebPath(base, title string) WebPath {
// TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory.
// So we do not add the support for writing slashes in title at the moment.
title = strings.TrimSpace(title)
title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
if title == "" || title == "." {
title = "unnamed"
}
return WebPath(title)
}
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
subURL := string(wikiName)
_, title := WebPathToUserTitle(wikiName)
return &api.WikiPageMetaData{
Title: title,
HTMLURL: repo.HTMLURL() + "/wiki/" + subURL,
SubURL: subURL,
LastCommit: convert.ToWikiCommit(lastCommit),
}
}