forked from mirrors/forgejo
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
|
// FullExtension returns the full extension of path, i.e. everything after and including
|
||||||
func GetRendererByFileName(filename string) Renderer {
|
// the first period in the basename of path.
|
||||||
extension := strings.ToLower(filepath.Ext(filename))
|
func FullExtension(path string) string {
|
||||||
return extRenderers[extension]
|
_, 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
|
// 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}
|
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
|
// ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
|
||||||
type ErrUnsupportedRenderExtension struct {
|
type ErrUnsupportedRenderExtension struct {
|
||||||
Extension string
|
Extension string
|
||||||
|
|
@ -365,8 +397,11 @@ func (err ErrUnsupportedRenderExtension) Error() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||||
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
|
extension := FullExtension(ctx.RelativePath)
|
||||||
if renderer, ok := extRenderers[extension]; ok {
|
if extension == "" {
|
||||||
|
return ErrMissingExtension{ctx.RelativePath}
|
||||||
|
}
|
||||||
|
if renderer := GetRendererByExtension(extension); renderer != nil {
|
||||||
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
|
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
|
||||||
if !ctx.InStandalonePage {
|
if !ctx.InStandalonePage {
|
||||||
// for an external render, it could only output its content in a standalone page
|
// 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
|
// Type returns if markup format via the filename
|
||||||
func Type(filename string) string {
|
func Type(filename string) string {
|
||||||
if parser := GetRendererByFileName(filename); parser != nil {
|
if parser := GetRendererByExtension(FullExtension(filename)); parser != nil {
|
||||||
return parser.Name()
|
return parser.Name()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -389,7 +424,7 @@ func Type(filename string) string {
|
||||||
|
|
||||||
// IsMarkupFile reports whether file is a markup type file
|
// IsMarkupFile reports whether file is a markup type file
|
||||||
func IsMarkupFile(name, markup string) bool {
|
func IsMarkupFile(name, markup string) bool {
|
||||||
if parser := GetRendererByFileName(name); parser != nil {
|
if parser := GetRendererByExtension(FullExtension(name)); parser != nil {
|
||||||
return parser.Name() == markup
|
return parser.Name() == markup
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ func (re *Renderer) RenderMarkup(ctx *context.Base, repo *context.Repository) {
|
||||||
Type: markupType,
|
Type: markupType,
|
||||||
RelativePath: relativePath,
|
RelativePath: relativePath,
|
||||||
}, strings.NewReader(re.Text), ctx.Resp); err != nil {
|
}, 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())
|
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,17 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forgejo.org/models/unit"
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/git"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
files_service "forgejo.org/services/repository/files"
|
||||||
"forgejo.org/tests"
|
"forgejo.org/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -38,3 +45,72 @@ func TestExternalMarkupRenderer(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
|
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]
|
[cache]
|
||||||
# Disable caching so that data isn't kept in-memory between test cases
|
# Disable caching so that data isn't kept in-memory between test cases
|
||||||
ITEM_TTL = -1
|
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]
|
[cache]
|
||||||
# Disable caching so that data isn't kept in-memory between test cases
|
# Disable caching so that data isn't kept in-memory between test cases
|
||||||
ITEM_TTL = -1
|
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]
|
[cache]
|
||||||
# Disable caching so that data isn't kept in-memory between test cases
|
# Disable caching so that data isn't kept in-memory between test cases
|
||||||
ITEM_TTL = -1
|
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