diff --git a/cli.go b/cli.go index c83ed93..d20b13d 100644 --- a/cli.go +++ b/cli.go @@ -2,9 +2,7 @@ package main import ( "fmt" - "net/http" "os" - "strings" "github.com/carlmjohnson/versioninfo" "github.com/spf13/cobra" @@ -143,62 +141,16 @@ func (a *App) svgCmd(cmd *cobra.Command, args []string) { } func (a *App) serveCmd(cmd *cobra.Command, args []string) { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - var focus []string - for _, e := range strings.Split(r.URL.Path, "+") { - focus = append(focus, strings.Trim(e, "/")) - } - - rendererName := r.URL.Query().Get("renderer") - if rendererName == "" { - rendererName = a.flags.serve.renderer - } - - cfg, err := NewConfig(a.flags.configfile) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(err.Error())) - return - } - - // build system - sys, errs := build(a.flags.base, a.flags.glob, focus) - if len(errs) > 0 { - w.WriteHeader(http.StatusInternalServerError) - out := "" - for _, err = range errs { - out += fmt.Sprintf("%s\n", err.Error()) - } - _, _ = w.Write([]byte(out)) - return - } - - // render template - renderer, ok := cfg.Renderer[rendererName] - if !ok { - exitOnErr(fmt.Errorf("renderer %s not specified in %s", rendererName, a.flags.configfile)) - } - out, err := render(sys, renderer) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(err.Error())) - return - } - - // create svg - img, err := svg(out) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(err.Error())) - return - } - - w.Header().Set("Content-Type", "image/svg+xml") - _, _ = w.Write(img) - }) - - fmt.Printf("server listening on http://%s/, hit CTRL-C to stop server...\n", a.flags.serve.listener) - _ = http.ListenAndServe(a.flags.serve.listener, nil) + s, err := NewServer( + a.flags.serve.listener, + a.flags.serve.renderer, + a.flags.configfile, + a.flags.base, + a.flags.glob, + ) + exitOnErr(err) + err = s.Run() + exitOnErr(err) } func (a *App) versionCmd(cmd *cobra.Command, args []string) { diff --git a/server.go b/server.go new file mode 100644 index 0000000..030ef83 --- /dev/null +++ b/server.go @@ -0,0 +1,117 @@ +package main + +import ( + "bytes" + "embed" + "fmt" + "io/fs" + "net/http" + "strings" + "text/template" +) + +//go:embed server/templates/* +var templateFS embed.FS + +//go:embed server/static/* +var staticFS embed.FS + +type server struct { + indexTemplate *template.Template + listener string + defaultRenderer string + configfile string + base string + glob string +} + +func NewServer(listener, defaultRenderer, configfile, base, glob string) (*server, error) { + s := &server{ + listener: listener, + defaultRenderer: defaultRenderer, + configfile: configfile, + base: base, + glob: glob, + } + var err error + s.indexTemplate, err = template.ParseFS(templateFS, "server/templates/index.html.tmpl") + return s, err +} + +func (s *server) Run() error { + http.HandleFunc("/index.html", s.HandleIndex) + + assets, err := fs.Sub(staticFS, "server") + if err != nil { + return err + } + fs := http.FileServer(http.FS(assets)) + http.Handle("/static/", fs) + + http.HandleFunc("/", s.HandleIndex) + + fmt.Printf("server listening on http://%s/, hit CTRL-C to stop server...\n", s.listener) + err = http.ListenAndServe(s.listener, nil) + return err +} + +func (s *server) HandleIndex(w http.ResponseWriter, r *http.Request) { + focusElems := r.URL.Query().Get("focus") + focus := strings.Split(focusElems, "_") + + rendererName := r.URL.Query().Get("renderer") + if rendererName == "" { + rendererName = s.defaultRenderer + } + + cfg, err := NewConfig(s.configfile) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + // build system + sys, errs := build(s.base, s.glob, focus) + if len(errs) > 0 { + w.WriteHeader(http.StatusInternalServerError) + out := "" + for _, err = range errs { + out += fmt.Sprintf("%s\n", err.Error()) + } + _, _ = w.Write([]byte(out)) + return + } + + // render template + renderer, ok := cfg.Renderer[rendererName] + if !ok { + exitOnErr(fmt.Errorf("renderer %s not specified in %s", rendererName, s.configfile)) + } + out, err := render(sys, renderer) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + // create svg + img, err := svg(out) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + var buf bytes.Buffer + + thing := strings.TrimSuffix(strings.TrimPrefix(string(img), `") + thing = ` 0 ? 1 / zoom.scaleFactor : zoom.scaleFactor; + + point.x = event.clientX; + point.y = event.clientY; + + var startPoint = point.matrixTransform(svg.getScreenCTM().inverse()); + + var fromVars = { + ease: zoom.ease, + x: viewBox.x, + y: viewBox.y, + width: viewBox.width, + height: viewBox.height, + }; + + viewBox.x -= (startPoint.x - viewBox.x) * (scaleDelta - 1); + viewBox.y -= (startPoint.y - viewBox.y) * (scaleDelta - 1); + viewBox.width *= scaleDelta; + viewBox.height *= scaleDelta; + + zoom.animation = TweenLite.from(viewBox, zoom.duration, fromVars); +} + +// +// SELECT DRAGGABLE +// =========================================================================== +function selectDraggable(event) { + + if (resetAnimation.isActive()) { + resetAnimation.kill(); + } + + startClient.x = this.pointerX; + startClient.y = this.pointerY; + startGlobal = startClient.matrixTransform(svg.getScreenCTM().inverse()); + + // Right mouse button + if (event.button === 2) { + + reachedThreshold = false; + + TweenLite.set(viewport, { + svgOrigin: startGlobal.x + " " + startGlobal.y + }); + + TweenLite.set(pivot, { + x: this.pointerX, + y: this.pointerY + }); + + pannable.disable(); + rotatable.enable().update().startDrag(event); + pivotAnimation.play(0); + + } else { + + TweenLite.set(proxy, { + x: this.pointerX, + y: this.pointerY + }); + + rotatable.disable(); + pannable.enable().update().startDrag(event); + pivotAnimation.reverse(); + } +} + +// +// RESET VIEWPORT +// =========================================================================== +function resetViewport() { + + var duration = 0.8; + var ease = Power3.easeOut; + + pivotAnimation.reverse(); + + if (pannable.tween) { + pannable.tween.kill(); + } + + if (rotatable.tween) { + rotatable.tween.kill(); + } + + resetAnimation.clear() + .to(viewBox, duration, { + x: cachedViewBox.x, + y: cachedViewBox.y, + width: cachedViewBox.width, + height: cachedViewBox.height, + ease: ease + }, 0) + .to(viewport, duration, { + attr: { transform: "matrix(1,0,0,1,0,0)" }, + // rotation: "0_short", + smoothOrigin: false, + svgOrigin: "0 0", + ease: ease + }, 0) +} + +// +// CHECK THRESHOLD +// =========================================================================== +function checkThreshold(value) { + + if (reachedThreshold) { + return value; + } + + var dx = Math.abs(this.pointerX - startClient.x); + var dy = Math.abs(this.pointerY - startClient.y); + + if (dx > rotateThreshold || dy > rotateThreshold || this.isThrowing) { + reachedThreshold = true; + return value; + } + + return this.rotation; +} + +// +// UPDATE VIEWBOX +// =========================================================================== +function updateViewBox() { + + if (zoom.animation.isActive()) { + return; + } + + point.x = this.x; + point.y = this.y; + + var moveGlobal = point.matrixTransform(svg.getScreenCTM().inverse()); + + viewBox.x -= (moveGlobal.x - startGlobal.x); + viewBox.y -= (moveGlobal.y - startGlobal.y); +} + diff --git a/server/static/style.css b/server/static/style.css new file mode 100644 index 0000000..1db2b67 --- /dev/null +++ b/server/static/style.css @@ -0,0 +1,63 @@ +body { + margin: 0; + padding: 0; + font-family: 'Karla', sans-serif; + font-weight: 400; +} + +.svg { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: move; +} + +.svg-scrim { + pointer-events: none; + z-index: 5; +} + +.proxy { + fill: none; + stroke: none; +} + +.svg-background { + fill: none; + stroke: none; +} + +.pivot { + fill: #ffc107; + stroke: rgba(0, 0, 0, 0.5); + stroke-width: 2; + opacity: 0; +} + +nav { + position: fixed; + bottom: 22px; + left: 18px; + right: 18px; + height: 26px; + padding: 12px; + z-index: 10; + background: rgba(230, 230, 230, 0.8); + border-radius: 25px; + box-shadow: 2px -2px 12px rgba(140, 140, 140, 0.8); +} + +.float-container {} + +.float-child { + margin: 0 20px; + float: right; +} + +.logo { + float: left; + font-weight: 700; + width: 80px; +} diff --git a/server/templates/index.html.tmpl b/server/templates/index.html.tmpl new file mode 100644 index 0000000..cd4627c --- /dev/null +++ b/server/templates/index.html.tmpl @@ -0,0 +1,36 @@ + + + + + + + sysdoc + + + + + {{.}} + + + + + + + + + + + +