mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: match on compound filename extensions (#11439)
Instead of going with a single extension, extracted by `filepath.Ext()`, all possible extensions are now generated for a given filename, by splitting the filename using a "." separator, starting with the longest candidate. Moreover, each extension candidate is matched against the actual set of known renderers (`extRenderers`), and only the longest matching extension is used. Resolves https://codeberg.org/forgejo/forgejo/issues/5190. Co-authored-by: Michael Hanke <michael.hanke@gmail.com> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11439 Reviewed-by: Ellen Εμιλία Άννα Zscheile <fogti@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Matthias Riße <matrss@0px.xyz> Co-committed-by: Matthias Riße <matrss@0px.xyz>
This commit is contained in:
parent
5b47f1f002
commit
04dd8ae525
6 changed files with 150 additions and 9 deletions
|
|
@ -198,10 +198,28 @@ func RegisterRenderer(renderer Renderer) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetRendererByFileName get renderer by filename
|
||||
func GetRendererByFileName(filename string) Renderer {
|
||||
extension := strings.ToLower(filepath.Ext(filename))
|
||||
return extRenderers[extension]
|
||||
// FullExtension returns the full extension of path, i.e. everything after and including
|
||||
// the first period in the basename of path.
|
||||
func FullExtension(path string) string {
|
||||
_, extension, found := strings.Cut(strings.ToLower(filepath.Base(path)), ".")
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
return "." + extension
|
||||
}
|
||||
|
||||
// GetRendererByExtension returns the most specific registered renderer for extension.
|
||||
func GetRendererByExtension(extension string) Renderer {
|
||||
_, extension, found := strings.Cut(extension, ".")
|
||||
checkedExtensions := 0
|
||||
for found && checkedExtensions < 10 {
|
||||
if renderer, ok := extRenderers["."+extension]; ok {
|
||||
return renderer
|
||||
}
|
||||
checkedExtensions++
|
||||
_, extension, found = strings.Cut(extension, ".")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRendererByType returns a renderer according type
|
||||
|
|
@ -350,6 +368,20 @@ func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
|||
return ErrUnsupportedRenderType{ctx.Type}
|
||||
}
|
||||
|
||||
// ErrMissingExtension represents the error when a path does not have any extension.
|
||||
type ErrMissingExtension struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func IsErrMissingExtension(err error) bool {
|
||||
_, ok := err.(ErrMissingExtension)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrMissingExtension) Error() string {
|
||||
return fmt.Sprintf("path '%s' does not have an extension", err.Path)
|
||||
}
|
||||
|
||||
// ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
|
||||
type ErrUnsupportedRenderExtension struct {
|
||||
Extension string
|
||||
|
|
@ -365,8 +397,11 @@ func (err ErrUnsupportedRenderExtension) Error() string {
|
|||
}
|
||||
|
||||
func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
|
||||
if renderer, ok := extRenderers[extension]; ok {
|
||||
extension := FullExtension(ctx.RelativePath)
|
||||
if extension == "" {
|
||||
return ErrMissingExtension{ctx.RelativePath}
|
||||
}
|
||||
if renderer := GetRendererByExtension(extension); renderer != nil {
|
||||
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
|
||||
if !ctx.InStandalonePage {
|
||||
// for an external render, it could only output its content in a standalone page
|
||||
|
|
@ -381,7 +416,7 @@ func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
|||
|
||||
// Type returns if markup format via the filename
|
||||
func Type(filename string) string {
|
||||
if parser := GetRendererByFileName(filename); parser != nil {
|
||||
if parser := GetRendererByExtension(FullExtension(filename)); parser != nil {
|
||||
return parser.Name()
|
||||
}
|
||||
return ""
|
||||
|
|
@ -389,7 +424,7 @@ func Type(filename string) string {
|
|||
|
||||
// IsMarkupFile reports whether file is a markup type file
|
||||
func IsMarkupFile(name, markup string) bool {
|
||||
if parser := GetRendererByFileName(name); parser != nil {
|
||||
if parser := GetRendererByExtension(FullExtension(name)); parser != nil {
|
||||
return parser.Name() == markup
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ func (re *Renderer) RenderMarkup(ctx *context.Base, repo *context.Repository) {
|
|||
Type: markupType,
|
||||
RelativePath: relativePath,
|
||||
}, strings.NewReader(re.Text), ctx.Resp); err != nil {
|
||||
if markup.IsErrUnsupportedRenderExtension(err) {
|
||||
if markup.IsErrUnsupportedRenderExtension(err) || markup.IsErrMissingExtension(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
|
|
|
|||
|
|
@ -7,10 +7,17 @@ import (
|
|||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
"forgejo.org/modules/setting"
|
||||
files_service "forgejo.org/services/repository/files"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -38,3 +45,72 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
||||
}
|
||||
|
||||
func TestExternalMarkupRendererWithCompoundFileExtensions(t *testing.T) {
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
testCases := []struct {
|
||||
path string
|
||||
shouldUseExternalRenderer bool
|
||||
expectedRenderedContent string
|
||||
}{
|
||||
{"foo.abc.def.ghi.jkl.md", true, ".def.ghi.jkl.md renderer used"},
|
||||
{"foo.def.ghi.jkl.md", true, ".def.ghi.jkl.md renderer used"},
|
||||
{"foo.ghi.jkl.md", true, ".ghi.jkl.md renderer used"},
|
||||
{"foo.jkl.md", false, "foo"},
|
||||
{"foo.md", false, "foo"},
|
||||
}
|
||||
|
||||
changeRepoFiles := []*files_service.ChangeRepoFile{}
|
||||
for _, testCase := range testCases {
|
||||
changeRepoFiles = append(changeRepoFiles,
|
||||
&files_service.ChangeRepoFile{
|
||||
Operation: "create",
|
||||
TreePath: testCase.path,
|
||||
ContentReader: strings.NewReader("foo"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
repo, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil)
|
||||
defer f()
|
||||
|
||||
_, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
|
||||
Files: changeRepoFiles,
|
||||
Message: "add files",
|
||||
Author: &files_service.IdentityOptions{
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.path, func(t *testing.T) {
|
||||
req := NewRequestf(t, "GET", "%s/src/branch/%s/%s", repo.HTMLURL(), repo.DefaultBranch, testCase.path)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
require.Equal(t, "text/html; charset=utf-8", resp.Header()["Content-Type"][0])
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
var query string
|
||||
if testCase.shouldUseExternalRenderer {
|
||||
query = "div.file-view"
|
||||
} else {
|
||||
query = "div.file-view p"
|
||||
}
|
||||
p := doc.Find(query)
|
||||
data, err := p.Html()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testCase.expectedRenderedContent, strings.TrimSpace(data))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,3 +140,13 @@ NEED_POSTPROCESS = false
|
|||
[cache]
|
||||
# Disable caching so that data isn't kept in-memory between test cases
|
||||
ITEM_TTL = -1
|
||||
|
||||
[markup.compound_file_extension]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .ghi.jkl.md
|
||||
RENDER_COMMAND = echo .ghi.jkl.md renderer used
|
||||
|
||||
[markup.compound_file_extension_2]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .def.ghi.jkl.md
|
||||
RENDER_COMMAND = echo .def.ghi.jkl.md renderer used
|
||||
|
|
|
|||
|
|
@ -154,3 +154,13 @@ NEED_POSTPROCESS = false
|
|||
[cache]
|
||||
# Disable caching so that data isn't kept in-memory between test cases
|
||||
ITEM_TTL = -1
|
||||
|
||||
[markup.compound_file_extension]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .ghi.jkl.md
|
||||
RENDER_COMMAND = echo .ghi.jkl.md renderer used
|
||||
|
||||
[markup.compound_file_extension_2]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .def.ghi.jkl.md
|
||||
RENDER_COMMAND = echo .def.ghi.jkl.md renderer used
|
||||
|
|
|
|||
|
|
@ -141,3 +141,13 @@ NEED_POSTPROCESS = false
|
|||
[cache]
|
||||
# Disable caching so that data isn't kept in-memory between test cases
|
||||
ITEM_TTL = -1
|
||||
|
||||
[markup.compound_file_extension]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .ghi.jkl.md
|
||||
RENDER_COMMAND = echo .ghi.jkl.md renderer used
|
||||
|
||||
[markup.compound_file_extension_2]
|
||||
ENABLED = true
|
||||
FILE_EXTENSIONS = .def.ghi.jkl.md
|
||||
RENDER_COMMAND = echo .def.ghi.jkl.md renderer used
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue