2017-06-22 23:37:46 +02:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2017-07-17 17:48:30 +02:00
|
|
|
"bytes"
|
2017-06-22 23:37:46 +02:00
|
|
|
"fmt"
|
2017-07-17 17:48:30 +02:00
|
|
|
"html"
|
2017-06-22 23:37:46 +02:00
|
|
|
"io"
|
|
|
|
"mime"
|
|
|
|
"net/http"
|
2017-07-17 17:48:30 +02:00
|
|
|
"os"
|
2017-06-22 23:37:46 +02:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/jaschaephraim/lrserver"
|
2022-01-29 20:17:23 +01:00
|
|
|
"github.com/danog/gojekyll/site"
|
|
|
|
"github.com/danog/liquid"
|
2017-06-22 23:37:46 +02:00
|
|
|
"github.com/pkg/browser"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Server serves the site on HTTP.
|
|
|
|
type Server struct {
|
2017-07-14 20:52:31 +02:00
|
|
|
sync.Mutex
|
2017-07-04 15:09:36 +02:00
|
|
|
Site *site.Site
|
2017-06-22 23:37:46 +02:00
|
|
|
lr *lrserver.Server
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run runs the server.
|
|
|
|
func (s *Server) Run(open bool, logger func(label, value string)) error {
|
2017-07-07 21:28:51 +02:00
|
|
|
cfg := s.Site.Config()
|
2017-06-29 00:44:39 +02:00
|
|
|
s.Site.SetAbsoluteURL("")
|
2017-07-07 21:28:51 +02:00
|
|
|
address := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
2017-06-23 15:32:08 +02:00
|
|
|
logger("Server address:", "http://"+address+"/")
|
2017-07-15 00:40:09 +02:00
|
|
|
if cfg.Watch {
|
2017-07-26 14:01:09 +02:00
|
|
|
if err := s.startLiveReloader(); err != nil {
|
2017-07-15 00:40:09 +02:00
|
|
|
return err
|
|
|
|
}
|
2017-07-21 22:29:38 +02:00
|
|
|
if err := s.watchReload(); err != nil {
|
2017-07-15 00:40:09 +02:00
|
|
|
return err
|
|
|
|
}
|
2017-06-22 23:37:46 +02:00
|
|
|
}
|
|
|
|
http.HandleFunc("/", s.handler)
|
|
|
|
c := make(chan error)
|
|
|
|
go func() {
|
|
|
|
c <- http.ListenAndServe(address, nil)
|
|
|
|
}()
|
|
|
|
logger("Server running...", "press ctrl-c to stop.")
|
|
|
|
if open {
|
|
|
|
if err := browser.OpenURL("http://" + address); err != nil {
|
|
|
|
fmt.Println("Error opening page:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return <-c
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) handler(rw http.ResponseWriter, r *http.Request) {
|
2017-07-14 20:52:31 +02:00
|
|
|
s.Lock()
|
|
|
|
defer s.Unlock()
|
|
|
|
|
2017-06-29 00:44:39 +02:00
|
|
|
var (
|
2017-07-13 03:17:11 +02:00
|
|
|
site = s.Site
|
|
|
|
urlpath = r.URL.Path
|
|
|
|
p, found = site.URLPage(urlpath)
|
2017-06-29 00:44:39 +02:00
|
|
|
)
|
2017-06-22 23:37:46 +02:00
|
|
|
if !found {
|
|
|
|
rw.WriteHeader(http.StatusNotFound)
|
2017-07-14 20:52:31 +02:00
|
|
|
p, found = site.Routes["/404.html"]
|
2017-06-22 23:37:46 +02:00
|
|
|
}
|
|
|
|
if !found {
|
2017-07-14 20:58:13 +02:00
|
|
|
fmt.Fprintf(rw, "404 page not found: %s\n", urlpath) // nolint: gas
|
2017-06-22 23:37:46 +02:00
|
|
|
return
|
|
|
|
}
|
2017-06-23 15:32:08 +02:00
|
|
|
mimeType := mime.TypeByExtension(p.OutputExt())
|
2017-06-22 23:37:46 +02:00
|
|
|
if mimeType != "" {
|
|
|
|
rw.Header().Set("Content-Type", mimeType)
|
|
|
|
}
|
|
|
|
var w io.Writer = rw
|
|
|
|
if strings.HasPrefix(mimeType, "text/html;") {
|
|
|
|
w = NewLiveReloadInjector(w)
|
|
|
|
}
|
2017-07-09 15:37:23 +02:00
|
|
|
err := site.WriteDocument(w, p)
|
2017-06-22 23:37:46 +02:00
|
|
|
if err != nil {
|
2017-07-17 17:48:30 +02:00
|
|
|
fmt.Fprintf(os.Stderr, "Error rendering %s: %s\n", urlpath, err)
|
|
|
|
eng := liquid.NewEngine()
|
|
|
|
excerpt, path := fileErrorContext(err)
|
|
|
|
out, e := eng.ParseAndRenderString(renderErrorTemplate, liquid.Bindings{
|
2017-07-27 00:55:24 +02:00
|
|
|
"error": fmt.Sprint(err),
|
2017-07-17 17:48:30 +02:00
|
|
|
"excerpt": excerpt,
|
|
|
|
"path": path,
|
|
|
|
"watch": site.Config().Watch,
|
|
|
|
})
|
|
|
|
if e != nil {
|
|
|
|
panic(e)
|
|
|
|
}
|
|
|
|
if _, err := io.WriteString(w, out); err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "Error writing HTTP response: %s", err)
|
|
|
|
}
|
2017-06-22 23:37:46 +02:00
|
|
|
}
|
|
|
|
}
|
2017-07-17 17:48:30 +02:00
|
|
|
|
|
|
|
func fileErrorContext(e error) (s, path string) {
|
|
|
|
cause, ok := e.(liquid.SourceError)
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
path, n := cause.Path(), cause.LineNumber()
|
2023-03-03 21:58:41 +01:00
|
|
|
b, err := os.ReadFile(path)
|
2017-07-17 17:48:30 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
|
|
|
|
l0, l1 := n-4, n+4
|
|
|
|
w := new(bytes.Buffer)
|
|
|
|
for i := l0; i < l1; i++ {
|
|
|
|
if i < 0 || len(lines) <= i {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var class string
|
|
|
|
if i+1 == n {
|
|
|
|
class = "error"
|
|
|
|
}
|
|
|
|
fmt.Fprintf(w, `<span class="line %s"><span class="gutter"></span><span class="lineno">%4d</span>%s<br /></span>`, class, i+1, html.EscapeString(lines[i]))
|
|
|
|
}
|
|
|
|
return w.String(), path
|
|
|
|
}
|
|
|
|
|
|
|
|
// CSS theme adapted from github.com/facebookincubator/create-react-app
|
|
|
|
const renderErrorTemplate = `<html><head>
|
|
|
|
<style type="text/css">
|
|
|
|
body { background-color: black; color: rgb(232, 232, 232); font-family: Menlo, Consolas, monospace; padding: 2rem; line-height: 1.2; }
|
|
|
|
h1 { color: #E36049 }
|
|
|
|
div { margin: 20px 0; }
|
|
|
|
code { font-size: xx-large; }
|
|
|
|
.line.error .gutter::before { content: "⚠️"; width: 0; float:left; }
|
|
|
|
.line.error, .line.error .lineno { color: red; }
|
2017-07-27 00:55:24 +02:00
|
|
|
.lineno { color: #6D7891; border-right: 1px solid #6D7891; padding-right: 10px; margin: 0 10px 0 5px; display: inline-block; text-align: right; width: 3em; }
|
2017-07-17 17:48:30 +02:00
|
|
|
footer { border-top: 1px solid #6D7891; margin-top: 5ex; padding-top: 5px; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Failed to render.</h1>
|
|
|
|
<div>{{ error }}:</div>
|
|
|
|
<code>{{ excerpt }}</code>
|
|
|
|
{% if watch and path != "" %}
|
|
|
|
<footer>Edit and save “{{ path }}” to reload this page.</footer>
|
|
|
|
{% endif %}
|
|
|
|
</body>
|
|
|
|
</html>`
|