Embedding light dark and auto (#718)

This commit is contained in:
Marcel Herrguth
2026-06-19 19:07:22 +02:00
committed by GitHub
parent 1ba588c90e
commit 9d9b54a5e1
4 changed files with 105 additions and 16 deletions
+9
View File
@@ -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>
```
+58 -16
View File
@@ -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
+37
View File
@@ -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"])
})
+1
View File
@@ -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")