mirror of
https://github.com/thomiceli/opengist.git
synced 2026-06-23 04:10:18 +00:00
Embedding light dark and auto (#718)
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Embed a Gist to your webpage
|
||||
|
||||
> [!Tip]
|
||||
> Fancy to enforce light or dark mode on the embedded Gist?
|
||||
> Just append `?light` or `?dark` to the Gist-URL.
|
||||
> Omitting this parameter will cause OpenGist to fallback to `auto`, thus the Browser deciding on the users preference.
|
||||
|
||||
To embed a Gist to your webpage, you can add a script tag with the URL of your gist followed by `.js` to your HTML page:
|
||||
|
||||
```html
|
||||
@@ -7,6 +12,8 @@ To embed a Gist to your webpage, you can add a script tag with the URL of your g
|
||||
|
||||
<!-- Dark mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?dark"></script>
|
||||
<!-- Light mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?light"></script>
|
||||
```
|
||||
|
||||
If you have a Gist that holds several different files, you can also explicitely call a specific file by its filename:
|
||||
@@ -16,4 +23,6 @@ If you have a Gist that holds several different files, you can also explicitely
|
||||
|
||||
<!-- Dark mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?file=filename&dark"></script>
|
||||
<!-- Light mode: -->
|
||||
<script src="http://opengist.url/user/gist-url.js?file=filename&light"></script>
|
||||
```
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
gojson "encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
@@ -98,9 +99,10 @@ func GistJson(ctx *context.Context) error {
|
||||
themeSep = "&"
|
||||
}
|
||||
jsUrl := jsBaseUrl + fileQuery
|
||||
cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["ts/embed.ts"].Css[0])
|
||||
baseHttpUrl := ctx.GetData("baseHttpUrl").(string)
|
||||
cssUrl, err := manifestCssUrl(baseHttpUrl, "ts/embed.ts")
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error joining css url", err)
|
||||
return ctx.ErrorRes(500, "Missing embed CSS in manifest", err)
|
||||
}
|
||||
|
||||
return ctx.JSON(200, map[string]interface{}{
|
||||
@@ -114,19 +116,30 @@ func GistJson(ctx *context.Context) error {
|
||||
"files": renderedFiles,
|
||||
"topics": topics,
|
||||
"embed": map[string]string{
|
||||
"html": htmlbuf.String(),
|
||||
"css": cssUrl,
|
||||
"js": jsUrl,
|
||||
"js_dark": jsUrl + themeSep + "dark",
|
||||
"html": htmlbuf.String(),
|
||||
"css": cssUrl,
|
||||
"js": jsUrl,
|
||||
"js_dark": jsUrl + themeSep + "dark",
|
||||
"js_light": jsUrl + themeSep + "light",
|
||||
"js_auto": jsUrl + themeSep + "auto",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func GistJs(ctx *context.Context) error {
|
||||
theme := "light"
|
||||
if _, exists := ctx.QueryParams()["dark"]; exists {
|
||||
params := ctx.QueryParams()
|
||||
_, hasDark := params["dark"]
|
||||
_, hasLight := params["light"]
|
||||
|
||||
theme := "auto"
|
||||
autoMode := true
|
||||
if hasDark {
|
||||
ctx.SetData("dark", "dark")
|
||||
theme = "dark"
|
||||
autoMode = false
|
||||
} else if hasLight {
|
||||
theme = "light"
|
||||
autoMode = false
|
||||
}
|
||||
|
||||
gist := ctx.GetData("gist").(*db.Gist)
|
||||
@@ -163,17 +176,18 @@ func GistJs(ctx *context.Context) error {
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
cssUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["ts/embed.ts"].Css[0])
|
||||
baseHttpUrl := ctx.GetData("baseHttpUrl").(string)
|
||||
cssUrl, err := manifestCssUrl(baseHttpUrl, "ts/embed.ts")
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error joining css url", err)
|
||||
return ctx.ErrorRes(500, "Missing embed CSS in manifest", err)
|
||||
}
|
||||
|
||||
themeUrl, err := url.JoinPath(ctx.GetData("baseHttpUrl").(string), context.ManifestEntries["ts/"+theme+".ts"].Css[0])
|
||||
themeUrl, err := manifestCssUrl(baseHttpUrl, "ts/"+theme+".ts")
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error joining theme url", err)
|
||||
return ctx.ErrorRes(500, "Missing theme CSS in manifest", err)
|
||||
}
|
||||
|
||||
js, err := escapeJavaScriptContent(htmlbuf.String(), cssUrl, themeUrl)
|
||||
js, err := escapeJavaScriptContent(htmlbuf.String(), cssUrl, themeUrl, autoMode)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error escaping JavaScript content", err)
|
||||
}
|
||||
@@ -192,7 +206,22 @@ func Preview(ctx *context.Context) error {
|
||||
return ctx.PlainText(200, previewStr)
|
||||
}
|
||||
|
||||
func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, error) {
|
||||
// manifestCssUrl returns the full CSS URL for a vite manifest key (e.g. "ts/embed.ts").
|
||||
// In dev mode (ManifestEntries is nil) it falls back to the vite dev server, deriving the
|
||||
// CSS path from the TS key: "ts/embed.ts" → "http://localhost:16157/css/embed.css".
|
||||
func manifestCssUrl(baseHttpUrl, key string) (string, error) {
|
||||
if context.ManifestEntries == nil {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(key, "ts/"), ".ts")
|
||||
return "http://localhost:16157/css/" + name + ".css", nil
|
||||
}
|
||||
entry, ok := context.ManifestEntries[key]
|
||||
if !ok || len(entry.Css) == 0 {
|
||||
return "", fmt.Errorf("no CSS entry for manifest key %q", key)
|
||||
}
|
||||
return url.JoinPath(baseHttpUrl, entry.Css[0])
|
||||
}
|
||||
|
||||
func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string, autoMode bool) (string, error) {
|
||||
jsonContent, err := gojson.Marshal(htmlContent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode content: %w", err)
|
||||
@@ -208,6 +237,11 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
return "", fmt.Errorf("failed to encode Theme URL: %w", err)
|
||||
}
|
||||
|
||||
jsonAutoMode, err := gojson.Marshal(autoMode)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode auto mode: %w", err)
|
||||
}
|
||||
|
||||
js := fmt.Sprintf(`
|
||||
(function() {
|
||||
if (!customElements.get('opengist-embed')) {
|
||||
@@ -217,7 +251,7 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
init(css1, css2, content) {
|
||||
init(css1, css2, content, autoMode) {
|
||||
this.shadowRoot.innerHTML = %s
|
||||
<style>
|
||||
@import url(${css1});
|
||||
@@ -226,6 +260,13 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
</style>
|
||||
<div class="container">${content}</div>
|
||||
%s;
|
||||
if (autoMode) {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const htmlDiv = this.shadowRoot.querySelector('.html');
|
||||
const applyTheme = () => htmlDiv && htmlDiv.classList.toggle('dark', mq.matches);
|
||||
mq.addEventListener('change', applyTheme);
|
||||
applyTheme();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -236,7 +277,7 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
})();
|
||||
|
||||
const instance = document.createElement('opengist-embed');
|
||||
instance.init(%s, %s, %s);
|
||||
instance.init(%s, %s, %s, %s);
|
||||
currentScript.parentNode.insertBefore(instance, currentScript.nextSibling);
|
||||
})();
|
||||
`,
|
||||
@@ -245,6 +286,7 @@ func escapeJavaScriptContent(htmlContent, cssUrl, themeUrl string) (string, erro
|
||||
string(jsonCssUrl),
|
||||
string(jsonThemeUrl),
|
||||
string(jsonContent),
|
||||
string(jsonAutoMode),
|
||||
)
|
||||
|
||||
return js, nil
|
||||
|
||||
@@ -179,6 +179,8 @@ func TestGistJson(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
require.Contains(t, embed["js"], identifier+".js")
|
||||
require.Contains(t, embed["js_dark"], identifier+".js?dark")
|
||||
require.Contains(t, embed["js_light"], identifier+".js?light")
|
||||
require.Contains(t, embed["js_auto"], identifier+".js?auto")
|
||||
require.NotEmpty(t, embed["css"])
|
||||
require.NotEmpty(t, embed["html"])
|
||||
})
|
||||
@@ -347,6 +349,39 @@ func TestGistJsSingleFile(t *testing.T) {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "dark.css")
|
||||
assert.Contains(t, string(body), ", false)")
|
||||
})
|
||||
|
||||
t.Run("LightTheme", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+".js?light", nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "light.css")
|
||||
assert.Contains(t, string(body), ", false)")
|
||||
})
|
||||
|
||||
t.Run("AutoTheme", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
// explicit ?auto
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+".js?auto", nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "auto.css")
|
||||
assert.Contains(t, string(body), "prefers-color-scheme")
|
||||
})
|
||||
|
||||
t.Run("DefaultThemeIsAuto", func(t *testing.T) {
|
||||
_, _, username, identifier := s.CreateGist(t, "0")
|
||||
|
||||
// no param → auto
|
||||
resp := s.Request(t, "GET", "/"+username+"/"+identifier+".js", nil, 200)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "auto.css")
|
||||
assert.Contains(t, string(body), "prefers-color-scheme")
|
||||
})
|
||||
|
||||
t.Run("PrivateGist", func(t *testing.T) {
|
||||
@@ -403,6 +438,8 @@ func TestGistJsonSingleFile(t *testing.T) {
|
||||
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.Contains(t, embed["js_light"], identifier+".js?file=file.txt&light")
|
||||
assert.Contains(t, embed["js_auto"], identifier+".js?file=file.txt&auto")
|
||||
assert.NotEmpty(t, embed["html"])
|
||||
})
|
||||
|
||||
|
||||
@@ -221,6 +221,7 @@ func Setup(t *testing.T) *Server {
|
||||
"ts/embed.ts": {Css: []string{"assets/embed.css"}},
|
||||
"ts/light.ts": {Css: []string{"assets/light.css"}},
|
||||
"ts/dark.ts": {Css: []string{"assets/dark.css"}},
|
||||
"ts/auto.ts": {Css: []string{"assets/auto.css"}},
|
||||
}
|
||||
|
||||
tmpGitConfig := filepath.Join(tmpDir, "gitconfig")
|
||||
|
||||
Reference in New Issue
Block a user