mirror of
https://github.com/thomiceli/opengist.git
synced 2026-06-19 07:36:56 +00:00
Feature: Allow embedding Gists for a certain file only (#709)
* Feature: Allow embedding Gists for a certain file only * Move from URL to param approach * Switch gist.Files to gist.File * Satisfy linting
This commit is contained in:
@@ -9,3 +9,11 @@ To embed a Gist to your webpage, you can add a script tag with the URL of your g
|
||||
<script src="http://opengist.url/user/gist-url.js?dark"></script>
|
||||
```
|
||||
|
||||
If you have a Gist that holds several different files, you can also explicitely call a specific file by its filename:
|
||||
|
||||
```html
|
||||
<script src="http://opengist.url/user/gist-url.js?file=filename"></script>
|
||||
|
||||
<!-- Dark mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?file=filename&dark"></script>
|
||||
```
|
||||
|
||||
@@ -48,9 +48,26 @@ func GistIndex(ctx *context.Context) error {
|
||||
|
||||
func GistJson(ctx *context.Context) error {
|
||||
gist := ctx.GetData("gist").(*db.Gist)
|
||||
files, hasMoreFiles, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error fetching files", err)
|
||||
|
||||
var files []*git.File
|
||||
hasMoreFiles := false
|
||||
embedFile := ctx.QueryParam("file")
|
||||
|
||||
if embedFile != "" {
|
||||
file, err := gist.File("HEAD", embedFile, true)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error fetching file", err)
|
||||
}
|
||||
if file == nil {
|
||||
return ctx.NotFound("File not found")
|
||||
}
|
||||
files = []*git.File{file}
|
||||
} else {
|
||||
var err error
|
||||
files, hasMoreFiles, err = gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error fetching files", err)
|
||||
}
|
||||
}
|
||||
|
||||
renderedFiles := render.RenderFiles(files)
|
||||
@@ -69,11 +86,19 @@ func GistJson(ctx *context.Context) error {
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
jsUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
|
||||
jsBaseUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), gist.User.Username, gist.Identifier()+".js")
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error joining js url", err)
|
||||
}
|
||||
|
||||
// Build per-file and per-theme URL variants.
|
||||
fileQuery, themeSep := "", "?"
|
||||
if embedFile != "" {
|
||||
fileQuery = "?file=" + url.QueryEscape(embedFile)
|
||||
themeSep = "&"
|
||||
}
|
||||
jsUrl := jsBaseUrl + fileQuery
|
||||
|
||||
cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["embed.css"].File)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error joining css url", err)
|
||||
@@ -93,7 +118,7 @@ func GistJson(ctx *context.Context) error {
|
||||
"html": htmlbuf.String(),
|
||||
"css": cssUrl,
|
||||
"js": jsUrl,
|
||||
"js_dark": jsUrl + "?dark",
|
||||
"js_dark": jsUrl + themeSep + "dark",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -106,9 +131,26 @@ func GistJs(ctx *context.Context) error {
|
||||
}
|
||||
|
||||
gist := ctx.GetData("gist").(*db.Gist)
|
||||
files, hasMoreFiles, err := gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error fetching files", err)
|
||||
|
||||
var files []*git.File
|
||||
hasMoreFiles := false
|
||||
embedFile := ctx.QueryParam("file")
|
||||
|
||||
if embedFile != "" {
|
||||
file, err := gist.File("HEAD", embedFile, true)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error fetching file", err)
|
||||
}
|
||||
if file == nil {
|
||||
return ctx.NotFound("File not found")
|
||||
}
|
||||
files = []*git.File{file}
|
||||
} else {
|
||||
var err error
|
||||
files, hasMoreFiles, err = gist.Files("HEAD", true)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error fetching files", err)
|
||||
}
|
||||
}
|
||||
|
||||
renderedFiles := render.RenderFiles(files)
|
||||
@@ -117,7 +159,7 @@ func GistJs(ctx *context.Context) error {
|
||||
|
||||
htmlbuf := bytes.Buffer{}
|
||||
w := bufio.NewWriter(&htmlbuf)
|
||||
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", ctx.DataMap(), ctx); err != nil {
|
||||
if err := ctx.Echo().Renderer.Render(w, "gist_embed.html", ctx.DataMap(), ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = w.Flush()
|
||||
@@ -175,7 +217,7 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
|
||||
init(css1, css2, content) {
|
||||
this.shadowRoot.innerHTML = %s
|
||||
<style>
|
||||
@@ -196,7 +238,7 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
|
||||
const instance = document.createElement('opengist-embed');
|
||||
instance.init(%s, %s, %s);
|
||||
currentScript.parentNode.insertBefore(instance, currentScript.nextSibling);
|
||||
currentScript.parentNode.insertBefore(instance, currentScript.nextSibling);
|
||||
})();
|
||||
`,
|
||||
"`",
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
@@ -325,3 +326,131 @@ func TestGetGistCaseInsensitive(t *testing.T) {
|
||||
s.Request(t, "GET", "/THOMAS/"+strings.ToUpper(gist.Uuid), nil, 200)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGistJsSingleFile(t *testing.T) {
|
||||
setupManifestEntries()
|
||||
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
s.Register(t, "alice")
|
||||
|
||||
t.Run("RendersOnlyRequestedFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+".js?file=file.txt", nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
js := string(body)
|
||||
|
||||
assert.Contains(t, js, "opengist-embed")
|
||||
assert.Contains(t, js, "hello world")
|
||||
assert.NotContains(t, js, "other content")
|
||||
})
|
||||
|
||||
t.Run("NonExistentFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+".js?file=nonexistent.txt", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("DarkTheme", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+".js?dark", nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "dark.css")
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
// Anonymous — no token
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+".js?file=file.txt", nil, 404)
|
||||
|
||||
// Invalid token
|
||||
s.RequestWithHeaders(t, "GET", "/"+username+"/"+identifier+".js?file=file.txt", nil, 404,
|
||||
map[string]string{"Authorization": "Token invalidtoken"})
|
||||
|
||||
// Other user's valid token
|
||||
s.Login(t, "alice")
|
||||
aliceTok := s.CreateAccessToken(t, "alice-tok", db.ReadPermission, db.ReadPermission)
|
||||
s.Logout()
|
||||
s.RequestWithHeaders(t, "GET", "/"+username+"/"+identifier+".js?file=file.txt", nil, 404,
|
||||
map[string]string{"Authorization": "Token " + aliceTok})
|
||||
|
||||
// Owner's valid token
|
||||
s.Login(t, "thomas")
|
||||
ownerTok := s.CreateAccessToken(t, "owner-tok", db.ReadPermission, db.ReadPermission)
|
||||
s.Logout()
|
||||
resp := s.RequestWithHeaders(t, "GET", "/"+username+"/"+identifier+".js?file=file.txt", nil, 200,
|
||||
map[string]string{"Authorization": "Token " + ownerTok})
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "hello world")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGistJsonSingleFile(t *testing.T) {
|
||||
setupManifestEntries()
|
||||
|
||||
s := webtest.Setup(t)
|
||||
defer webtest.Teardown(t)
|
||||
|
||||
s.Register(t, "thomas")
|
||||
|
||||
t.Run("RendersOnlyRequestedFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+".json?file=file.txt", nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(body, &result))
|
||||
|
||||
files, ok := result["files"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, files, 1)
|
||||
assert.Equal(t, "file.txt", files[0].(map[string]interface{})["filename"])
|
||||
|
||||
embed, ok := result["embed"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Contains(t, embed["js"], identifier+".js?file=file.txt")
|
||||
assert.Contains(t, embed["js_dark"], identifier+".js?file=file.txt&dark")
|
||||
assert.NotEmpty(t, embed["html"])
|
||||
})
|
||||
|
||||
t.Run("NonExistentFile", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+".json?file=nonexistent.txt", nil, 404)
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "2")
|
||||
|
||||
// Anonymous — no token
|
||||
s.Request(t, "GET", "/"+username+"/"+identifier+".json?file=file.txt", nil, 404)
|
||||
|
||||
// Invalid token
|
||||
s.RequestWithHeaders(t, "GET", "/"+username+"/"+identifier+".json?file=file.txt", nil, 404,
|
||||
map[string]string{"Authorization": "Token invalidtoken"})
|
||||
|
||||
// Owner's valid token
|
||||
s.Login(t, "thomas")
|
||||
ownerTok := s.CreateAccessToken(t, "owner-tok", db.ReadPermission, db.ReadPermission)
|
||||
s.Logout()
|
||||
|
||||
resp := s.RequestWithHeaders(t, "GET", "/"+username+"/"+identifier+".json?file=file.txt", nil, 200,
|
||||
map[string]string{"Authorization": "Token " + ownerTok})
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(body, &result))
|
||||
files, ok := result["files"].([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, files, 1)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user