1diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go
2index ffa5dfd6230013ce209b1c3f51c866254e734e2e..d046d19e47cfa20f771d86c54764253a1c8b84b3 100644
3--- a/pkg/handler/git/handler.go
4+++ b/pkg/handler/git/handler.go
5@@ -349,6 +349,7 @@
6 formatter := html.New(
7 html.WithLineNumbers(true),
8 html.WithLinkableLineNumbers(true, "L"),
9+ html.WithClasses(true),
10 )
11
12 iterator, err := lexer.Tokenise(nil, string(file))
13@@ -440,6 +441,7 @@
14 formatter := html.New(
15 html.WithLineNumbers(true),
16 html.WithLinkableLineNumbers(true, "L"),
17+ html.WithClasses(true),
18 )
19
20 iterator, err := lexer.Tokenise(nil, diff)
21diff --git a/pkg/handler/router.go b/pkg/handler/router.go
22index fea882737aac94a9f22681315783e1831bfe9663..bc81350d19a5296cae5d8da058bac29a69bff693 100644
23--- a/pkg/handler/router.go
24+++ b/pkg/handler/router.go
25@@ -31,6 +31,14 @@ if err != nil {
26 return nil, err
27 }
28
29+ cssStaticHandler, err := static.ServeStaticCSSHandler(
30+ configRepo.GetSyntaxHighlight(),
31+ configRepo.GetSyntaxHighlightDark(),
32+ )
33+ if err != nil {
34+ return nil, err
35+ }
36+
37 mux := ext.NewRouter()
38 mux.AddMiddleware(ext.Compress)
39 mux.AddMiddleware(ext.Log)
40@@ -45,6 +53,7 @@ mux.AddMiddleware(ext.DisableAuthentication)
41 }
42
43 mux.HandleFunc("/static/{file}", staticHandler)
44+ mux.HandleFunc("/static/theme", cssStaticHandler)
45 mux.HandleFunc("/{name}/about/{$}", gitHandler.About)
46 mux.HandleFunc("/{name}", gitHandler.Multiplex)
47 mux.HandleFunc("/{name}/{rest...}", gitHandler.Multiplex)
48diff --git a/pkg/handler/static/handler.go b/pkg/handler/static/handler.go
49index cdb2ae6d4b81ebc5396a0e6ce9d01bd48a73e2e9..779c78660309a955ece24d643d21877b610c2cad 100644
50--- a/pkg/handler/static/handler.go
51+++ b/pkg/handler/static/handler.go
52@@ -1,6 +1,8 @@
53 package static
54
55 import (
56+ "fmt"
57+ "io"
58 "io/fs"
59 "mime"
60 "net/http"
61@@ -8,6 +10,9 @@ "path/filepath"
62
63 "git.gabrielgio.me/cerrado/pkg/ext"
64 "git.gabrielgio.me/cerrado/static"
65+ "github.com/alecthomas/chroma/v2"
66+ "github.com/alecthomas/chroma/v2/formatters/html"
67+ "github.com/alecthomas/chroma/v2/styles"
68 )
69
70 func ServeStaticHandler() (ext.ErrorRequestHandler, error) {
71@@ -28,3 +33,47 @@ http.ServeFileFS(w, r.Request, staticFs, f)
72 return nil
73 }, nil
74 }
75+
76+func ServeStaticCSSHandler(lightTheme, darkTheme string) (ext.ErrorRequestHandler, error) {
77+ var (
78+ lightStyle = styles.Get(lightTheme)
79+ darkStyle = styles.Get(darkTheme)
80+ formatter = html.New(
81+ html.WithCSSComments(false),
82+ )
83+ )
84+
85+ return func(w http.ResponseWriter, r *ext.Request) error {
86+ ext.SetMIME(w, "text/css")
87+
88+ var style *chroma.Style
89+ style = darkStyle
90+ w.Write([]byte("[data-bs-theme=\"dark\"] {\n"))
91+ err := formatter.WriteCSS(&ws{w}, style)
92+ if err != nil {
93+ return err
94+ }
95+ w.Write([]byte("}\n"))
96+
97+ style = lightStyle
98+ w.Write([]byte("[data-bs-theme=\"light\"] {\n"))
99+ err = formatter.WriteCSS(&ws{w}, style)
100+ if err != nil {
101+ return err
102+ }
103+ w.Write([]byte("\n}"))
104+
105+ return nil
106+ }, nil
107+}
108+
109+type ws struct {
110+ inner io.Writer
111+}
112+
113+// This is very cursed, and rely on the fact that it writes every css rule at time.
114+// it adds & to the begging so it can be nested by the ServeStaticCSSHandler.
115+// This will allow the follow bootstrap data-bs-theme.
116+func (w *ws) Write(p []byte) (n int, err error) {
117+ return fmt.Fprintf(w.inner, "& %s", string(p))
118+}
119diff --git a/templates/base.qtpl b/templates/base.qtpl
120index b3df94a5b8b51f047b1403ddea94a376122fd844..e43fb67fa5b7c74e9a0caa9057788cf46a4a614e 100644
121--- a/templates/base.qtpl
122+++ b/templates/base.qtpl
123@@ -60,6 +60,8 @@ <meta charset="utf-8">
124 <link rel="icon" href="data:,">
125 <title>{%= p.Title(ctx) %}</title>
126 <link rel="stylesheet" href="/static/main{%s Slug %}.css">
127+ <link rel="stylesheet" href="/static/themes/dark">
128+ <link rel="stylesheet" href="/static/themes/light">
129 <html data-bs-theme="dark">
130 <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
131 <meta name="viewport" content="width=device-width, initial-scale=1" />
132diff --git a/templates/base.qtpl.go b/templates/base.qtpl.go
133index dce4cbcf1c80f70254c7a2dc5bcbc3794414fc2d..783de2c10402a1a33125b1f872bc3db488ede7ab 100644
134--- a/templates/base.qtpl.go
135+++ b/templates/base.qtpl.go
136@@ -8,16 +8,14 @@ //line templates/base.qtpl:3
137 package templates
138
139 //line templates/base.qtpl:3
140-import "context"
141-
142-//line templates/base.qtpl:4
143-import "strconv"
144+import (
145+ "context"
146+ "strconv"
147+ "time" //line templates/base.qtpl:4
148
149-//line templates/base.qtpl:5
150-import "time"
151+ //line templates/base.qtpl:5
152+ //line templates/base.qtpl:7
153
154-//line templates/base.qtpl:7
155-import (
156 qtio422016 "io"
157
158 qt422016 "github.com/valyala/quicktemplate"
159@@ -112,59 +110,60 @@ //line templates/base.qtpl:62
160 qw422016.E().S(Slug)
161 //line templates/base.qtpl:62
162 qw422016.N().S(`.css">
163+ <link rel="stylesheet" href="/static/theme">
164 <html data-bs-theme="dark">
165 <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
166 <meta name="viewport" content="width=device-width, initial-scale=1" />
167 </head>
168 <body>
169 `)
170-//line templates/base.qtpl:68
171+//line templates/base.qtpl:70
172 p.StreamNavbar(qw422016, ctx)
173-//line templates/base.qtpl:68
174+//line templates/base.qtpl:70
175 qw422016.N().S(`
176 <div class="container">
177 `)
178-//line templates/base.qtpl:70
179+//line templates/base.qtpl:72
180 p.StreamContent(qw422016, ctx)
181-//line templates/base.qtpl:70
182+//line templates/base.qtpl:72
183 qw422016.N().S(`
184 </div>
185 </body>
186 `)
187-//line templates/base.qtpl:73
188+//line templates/base.qtpl:75
189 p.StreamScript(qw422016, ctx)
190-//line templates/base.qtpl:73
191+//line templates/base.qtpl:75
192 qw422016.N().S(`
193 <script>
194 function a(){const e=window.matchMedia("(prefers-color-scheme: dark)").matches;document.documentElement.setAttribute("data-bs-theme",e?"dark":"light")}a(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",a);
195 </script>
196 </html>
197 `)
198-//line templates/base.qtpl:78
199+//line templates/base.qtpl:80
200 }
201
202-//line templates/base.qtpl:78
203+//line templates/base.qtpl:80
204 func WritePageTemplate(qq422016 qtio422016.Writer, p Page, ctx context.Context) {
205-//line templates/base.qtpl:78
206+//line templates/base.qtpl:80
207 qw422016 := qt422016.AcquireWriter(qq422016)
208-//line templates/base.qtpl:78
209+//line templates/base.qtpl:80
210 StreamPageTemplate(qw422016, p, ctx)
211-//line templates/base.qtpl:78
212+//line templates/base.qtpl:80
213 qt422016.ReleaseWriter(qw422016)
214-//line templates/base.qtpl:78
215+//line templates/base.qtpl:80
216 }
217
218-//line templates/base.qtpl:78
219+//line templates/base.qtpl:80
220 func PageTemplate(p Page, ctx context.Context) string {
221-//line templates/base.qtpl:78
222+//line templates/base.qtpl:80
223 qb422016 := qt422016.AcquireByteBuffer()
224-//line templates/base.qtpl:78
225+//line templates/base.qtpl:80
226 WritePageTemplate(qb422016, p, ctx)
227-//line templates/base.qtpl:78
228+//line templates/base.qtpl:80
229 qs422016 := string(qb422016.B)
230-//line templates/base.qtpl:78
231+//line templates/base.qtpl:80
232 qt422016.ReleaseByteBuffer(qb422016)
233-//line templates/base.qtpl:78
234+//line templates/base.qtpl:80
235 return qs422016
236-//line templates/base.qtpl:78
237+//line templates/base.qtpl:80
238 }