mirror of
https://gerrit.hackerspace.pl/hscloud
synced 2024-10-08 19:17:45 +00:00
devtools/hackdoc: init
This is hackdoc, a documentation rendering tool for monorepos. This is the first code iteration, that can only serve from a local git checkout. The code is incomplete, and is WIP. Change-Id: I68ef7a991191c1bb1b0fdd2a8d8353aba642e28f
This commit is contained in:
parent
154baf1cf6
commit
c881cf3c22
18 changed files with 898 additions and 24 deletions
24
README
24
README
|
@ -1,24 +0,0 @@
|
|||
HSCloud
|
||||
=======
|
||||
|
||||
This is a monorepo. You'll need bash and Bazel 1.0.0+ to use it.
|
||||
|
||||
If you have Nix installed you will also be able to manage bare metal nodes. If you don't want that, you can skip it.
|
||||
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
cd hscloud
|
||||
. env.sh # setup PATH and hscloud_root
|
||||
tools/install.sh # build tools
|
||||
|
||||
|
||||
Then, to get Kubernetes access to k0.hswaw.net (current nearly-production cluster):
|
||||
|
||||
prodaccess
|
||||
kubectl version
|
||||
|
||||
You will automatically get a `personal-$USERNAME` namespace created in which you have full admin rights.
|
||||
|
||||
For mor information about the cluster, see [cluster/README].
|
35
README.md
Normal file
35
README.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
hscloud
|
||||
=======
|
||||
|
||||
`hscloud` is the main monorepo of the Warsaw Hackerspace infrastructure code.
|
||||
|
||||
Any time you see a `//path/like/this`, it refers to the root of hscloud, ie. the path `path/like/this` in this repository. Perforce and/or Bazel users should feel right at home.
|
||||
|
||||
|
||||
Viewing this documentation
|
||||
--------------------------
|
||||
|
||||
For a please web viewing experience, [see this documentation in hackdoc](https://hackdoc.hackerspace.pl/). This will allow you to read this markdown file (and others) in a pretty, linkable view.
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
You will need Bash and Bazel (1.2.0+). Clone this repo, cd into it and:
|
||||
|
||||
. ./env.sh # setup PATH and hscloud_root
|
||||
tools/install.sh # build tools
|
||||
|
||||
A bunch of common tools will appearify in your `$PATH`. You should now be ready to follow other documentation.
|
||||
|
||||
This does not pollute your system, and you can work on multiple hscloud checkouts independently.
|
||||
|
||||
What now?
|
||||
---------
|
||||
|
||||
If you want to use our Kubernetes cluster to run some stuff, see [//cluster/doc/user](cluster/doc/user).
|
||||
|
||||
If you're looking for administrative docs about cluster maintenance, see [//cluster/doc/admin](cluster/doc/admin).
|
||||
|
||||
If you want to browse the source of `hscloud` in a web browser, use [gerrit's gitiles](https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/refs/heads/master/).
|
||||
|
||||
If you want to learn how to contribute to this repository, see [//doc/codelab/gerrit](doc/codelab/gerrit).
|
15
WORKSPACE
15
WORKSPACE
|
@ -191,8 +191,11 @@ http_archive(
|
|||
|
||||
# Invoke go_rules_dependencies depending on host platform.
|
||||
load("//tools:go_sdk.bzl", "gen_imports")
|
||||
|
||||
gen_imports(name = "go_sdk_imports")
|
||||
|
||||
load("@go_sdk_imports//:imports.bzl", "load_go_sdk")
|
||||
|
||||
load_go_sdk()
|
||||
|
||||
# Go Gazelle rules
|
||||
|
@ -1948,3 +1951,15 @@ go_repository(
|
|||
commit = "d07dcb9293789fdc99c797d3499a5799bc343b86",
|
||||
importpath = "gopkg.in/irc.v3",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "in_gopkg_russross_blackfriday_v2",
|
||||
commit = "d3b5b032dc8e8927d31a5071b56e14c89f045135",
|
||||
importpath = "gopkg.in/russross/blackfriday.v2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_shurcool_sanitized_anchor_name",
|
||||
commit = "7bfe4c7ecddb3666a94b053b422cdd8f5aaa3615",
|
||||
importpath = "github.com/shurcooL/sanitized_anchor_name",
|
||||
)
|
||||
|
|
24
devtools/hackdoc/BUILD.bazel
Normal file
24
devtools/hackdoc/BUILD.bazel
Normal file
|
@ -0,0 +1,24 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"helpers.go",
|
||||
"main.go",
|
||||
"markdown.go",
|
||||
],
|
||||
importpath = "code.hackerspace.pl/hscloud/devtools/hackdoc",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//devtools/hackdoc/config:go_default_library",
|
||||
"//devtools/hackdoc/source:go_default_library",
|
||||
"@com_github_golang_glog//:go_default_library",
|
||||
"@in_gopkg_russross_blackfriday_v2//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "hackdoc",
|
||||
embed = [":go_default_library"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
18
devtools/hackdoc/README.md
Normal file
18
devtools/hackdoc/README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
Hackdoc
|
||||
=======
|
||||
|
||||
Hackdoc is a tool to automatically serve documentation based on a checkout of the [hscloud](/) source.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Any Markdown submitted to hscloud is visible via hackdoc. Simply go to https://hackdoc.hackerspace.pl/path/to/markdown.md to see it rendered.
|
||||
|
||||
Local Rendering
|
||||
---------------
|
||||
|
||||
To run hackdoc locally on a filesystem checkout (ie. when working on docs, templates, or hackdoc itself), run:
|
||||
|
||||
bazel run //devtools/hackdoc:local
|
||||
|
||||
The output log should tell you where hackdoc just started listening at. Currently this is `127.0.0.1:8080` by default. You can change this by passing a `-listen` flag, eg. `-listen 127.0.0.1:4242`.
|
20
devtools/hackdoc/config/BUILD.bazel
Normal file
20
devtools/hackdoc/config/BUILD.bazel
Normal file
|
@ -0,0 +1,20 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["config.go"],
|
||||
importpath = "code.hackerspace.pl/hscloud/devtools/hackdoc/config",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//devtools/hackdoc/source:go_default_library",
|
||||
"@com_github_burntsushi_toml//:go_default_library",
|
||||
"@com_github_golang_glog//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["config_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = ["@com_github_go_test_deep//:go_default_library"],
|
||||
)
|
152
devtools/hackdoc/config/config.go
Normal file
152
devtools/hackdoc/config/config.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/golang/glog"
|
||||
|
||||
"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
|
||||
)
|
||||
|
||||
// Config is a configuration concerning a given path of the source. It is built
|
||||
// from files present in the source, and from global configuration.
|
||||
type Config struct {
|
||||
// DefaultIndex is the filenames that should attempt to be rendered if no exact path is given
|
||||
DefaultIndex []string
|
||||
// Templates are the templates available to render markdown files, keyed by template name.
|
||||
Templates map[string]*template.Template
|
||||
|
||||
// Errors that occured while building this config (due to config file errors, etc).
|
||||
Errors map[string]error
|
||||
}
|
||||
|
||||
type configToml struct {
|
||||
DefaultIndex []string `toml:"default_index"`
|
||||
Templates map[string]*configTomlTemplate `toml:"template"`
|
||||
}
|
||||
|
||||
type configTomlTemplate struct {
|
||||
Sources []string `toml:"sources"`
|
||||
}
|
||||
|
||||
func parseToml(data []byte) (*configToml, error) {
|
||||
var c configToml
|
||||
err := toml.Unmarshal(data, &c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.Templates == nil {
|
||||
c.Templates = make(map[string]*configTomlTemplate)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func configFileLocations(path string) []string {
|
||||
// Support for unix-style filesystem prefix (/foo/bar/baz) and
|
||||
// perforce-depot-style prefix (//foo/bar/baz).
|
||||
// Also support relative paths.
|
||||
pathTrimmed := strings.TrimLeft(path, "/")
|
||||
prefixLen := len(path) - len(pathTrimmed)
|
||||
prefix := path[:prefixLen]
|
||||
path = pathTrimmed
|
||||
if len(prefix) > 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Turn path into possible directory names, including root.
|
||||
path = strings.Trim(path, "/")
|
||||
parts := strings.Split(path, "/")
|
||||
if parts[0] != "" {
|
||||
parts = append([]string{""}, parts...)
|
||||
}
|
||||
|
||||
locations := []string{}
|
||||
for i, _ := range parts {
|
||||
p := strings.Join(parts[:i+1], "/")
|
||||
p += "/hackdoc.toml"
|
||||
p = prefix + strings.Trim(p, "/")
|
||||
locations = append(locations, p)
|
||||
}
|
||||
return locations
|
||||
}
|
||||
|
||||
func ForPath(s source.Source, path string) (*Config, error) {
|
||||
if path != "//" {
|
||||
path = strings.TrimRight(path, "/")
|
||||
}
|
||||
|
||||
// Try cache.
|
||||
cacheKey := fmt.Sprintf("config:%s", path)
|
||||
if v := s.CacheGet(cacheKey); v != nil {
|
||||
cfg, ok := v.(*Config)
|
||||
if !ok {
|
||||
glog.Errorf("Cache key %q corrupted, deleting", cacheKey)
|
||||
s.CacheSet([]string{}, cacheKey, nil)
|
||||
} else {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Feed cache.
|
||||
cfg := &Config{
|
||||
Templates: make(map[string]*template.Template),
|
||||
Errors: make(map[string]error),
|
||||
}
|
||||
|
||||
tomlPaths := configFileLocations(path)
|
||||
for _, p := range tomlPaths {
|
||||
file, err := s.IsFile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("IsFile(%q): %w", path, err)
|
||||
}
|
||||
if !file {
|
||||
continue
|
||||
}
|
||||
data, err := s.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadFile(%q): %w", path, err)
|
||||
}
|
||||
|
||||
c, err := parseToml(data)
|
||||
if err != nil {
|
||||
cfg.Errors[p] = err
|
||||
continue
|
||||
}
|
||||
|
||||
err = cfg.updateFromToml(p, s, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("updating from %q: %w", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *Config) updateFromToml(p string, s source.Source, t *configToml) error {
|
||||
if t.DefaultIndex != nil {
|
||||
c.DefaultIndex = t.DefaultIndex
|
||||
}
|
||||
|
||||
for k, v := range t.Templates {
|
||||
tmpl := template.New(k)
|
||||
|
||||
for _, source := range v.Sources {
|
||||
data, err := s.ReadFile(source)
|
||||
if err != nil {
|
||||
c.Errors[p] = fmt.Errorf("reading template file %q: %w", source, err)
|
||||
return nil
|
||||
}
|
||||
tmpl, err = tmpl.Parse(string(data))
|
||||
if err != nil {
|
||||
c.Errors[p] = fmt.Errorf("parsing template file %q: %w", source, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
c.Templates[k] = tmpl
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
99
devtools/hackdoc/config/config_test.go
Normal file
99
devtools/hackdoc/config/config_test.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
data string
|
||||
want *configToml
|
||||
}{
|
||||
{
|
||||
name: "normal config",
|
||||
data: `
|
||||
default_index = ["foo.md", "bar.md"]
|
||||
[template.default]
|
||||
sources = ["hackdoc/bar.html", "hackdoc/baz.html"]
|
||||
[template.foo]
|
||||
sources = ["foo/bar.html", "foo/baz.html"]
|
||||
`,
|
||||
want: &configToml{
|
||||
DefaultIndex: []string{"foo.md", "bar.md"},
|
||||
Templates: map[string]*configTomlTemplate{
|
||||
"default": &configTomlTemplate{
|
||||
Sources: []string{"hackdoc/bar.html", "hackdoc/baz.html"},
|
||||
},
|
||||
"foo": &configTomlTemplate{
|
||||
Sources: []string{"foo/bar.html", "foo/baz.html"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "empty config",
|
||||
data: "",
|
||||
want: &configToml{
|
||||
DefaultIndex: nil,
|
||||
Templates: map[string]*configTomlTemplate{},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := parseToml([]byte(test.data))
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse config: %w", err)
|
||||
}
|
||||
if diff := deep.Equal(test.want, got); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocations(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
path string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "perforce-style path",
|
||||
path: "//foo/bar/baz",
|
||||
want: []string{"//hackdoc.toml", "//foo/hackdoc.toml", "//foo/bar/hackdoc.toml", "//foo/bar/baz/hackdoc.toml"},
|
||||
}, {
|
||||
name: "unix-style path",
|
||||
path: "/foo/bar/baz",
|
||||
want: []string{"/hackdoc.toml", "/foo/hackdoc.toml", "/foo/bar/hackdoc.toml", "/foo/bar/baz/hackdoc.toml"},
|
||||
}, {
|
||||
name: "relative-style path",
|
||||
path: "foo/bar/baz",
|
||||
want: []string{"hackdoc.toml", "foo/hackdoc.toml", "foo/bar/hackdoc.toml", "foo/bar/baz/hackdoc.toml"},
|
||||
}, {
|
||||
name: "root perforce-style path",
|
||||
path: "//",
|
||||
want: []string{"//hackdoc.toml"},
|
||||
}, {
|
||||
name: "root unix-style path",
|
||||
path: "/",
|
||||
want: []string{"/hackdoc.toml"},
|
||||
}, {
|
||||
name: "empty path",
|
||||
path: "",
|
||||
want: []string{"hackdoc.toml"},
|
||||
}, {
|
||||
name: "weird path",
|
||||
path: "///what/is///this///",
|
||||
want: nil,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := configFileLocations(test.path)
|
||||
if diff := deep.Equal(test.want, got); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
18
devtools/hackdoc/helpers.go
Normal file
18
devtools/hackdoc/helpers.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handle404(w http.ResponseWriter, r *http.Request) {
|
||||
logRequest(w, r, "404")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "404!\n")
|
||||
}
|
||||
|
||||
func handle500(w http.ResponseWriter, r *http.Request) {
|
||||
logRequest(w, r, "500")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "500 :(\n")
|
||||
}
|
178
devtools/hackdoc/main.go
Normal file
178
devtools/hackdoc/main.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
|
||||
"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
|
||||
)
|
||||
|
||||
var (
|
||||
flagListen = "127.0.0.1:8080"
|
||||
flagDocRoot = "./docroot"
|
||||
flagHackdocURL = ""
|
||||
flagGitwebURLPattern = "https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/%s/%s"
|
||||
flagGitwebDefaultBranch = "master"
|
||||
|
||||
rePagePath = regexp.MustCompile(`^/([A-Za-z0-9_\-/\. ]*)$`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Set("logtostderr", "true")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&flagListen, "listen", flagListen, "Address to listen on for HTTP traffic")
|
||||
flag.StringVar(&flagDocRoot, "docroot", flagDocRoot, "Path from which to serve documents")
|
||||
flag.StringVar(&flagHackdocURL, "hackdoc_url", flagHackdocURL, "Public URL of hackdoc. If not given, autogenerate from listen path for dev purposes")
|
||||
flag.StringVar(&flagGitwebURLPattern, "gitweb_url_pattern", flagGitwebURLPattern, "Pattern to sprintf to for URL for viewing a file in Git. First string is ref/rev, second is bare file path (sans //)")
|
||||
flag.StringVar(&flagGitwebDefaultBranch, "gitweb_default_rev", flagGitwebDefaultBranch, "Default Git rev to render/link to")
|
||||
flag.Parse()
|
||||
|
||||
if flagHackdocURL == "" {
|
||||
flagHackdocURL = fmt.Sprintf("http://%s", flagListen)
|
||||
}
|
||||
|
||||
path, err := filepath.Abs(flagDocRoot)
|
||||
if err != nil {
|
||||
glog.Fatalf("Could not dereference path %q: %w", path, err)
|
||||
}
|
||||
|
||||
s := &service{
|
||||
source: source.NewLocal(path),
|
||||
}
|
||||
|
||||
http.HandleFunc("/", s.handler)
|
||||
|
||||
glog.Infof("Listening on %q...", flagListen)
|
||||
if err := http.ListenAndServe(flagListen, nil); err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type service struct {
|
||||
source source.Source
|
||||
}
|
||||
|
||||
func (s *service) handler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
fmt.Fprintf(w, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
glog.Infof("%+v", r.URL.Query())
|
||||
rev := r.URL.Query().Get("rev")
|
||||
if rev == "" {
|
||||
rev = flagGitwebDefaultBranch
|
||||
}
|
||||
|
||||
path := r.URL.Path
|
||||
|
||||
if match := rePagePath.FindStringSubmatch(path); match != nil {
|
||||
s.handlePage(w, r, rev, match[1])
|
||||
return
|
||||
}
|
||||
handle404(w, r)
|
||||
}
|
||||
|
||||
func logRequest(w http.ResponseWriter, r *http.Request, format string, args ...interface{}) {
|
||||
result := fmt.Sprintf(format, args...)
|
||||
glog.Infof("result: %s, remote: %q, ua: %q, referrer: %q, host: %q path: %q", result, r.RemoteAddr, r.Header.Get("User-Agent"), r.Header.Get("Referrer"), r.Host, r.URL.Path)
|
||||
}
|
||||
|
||||
func urlPathToDepotPath(url string) string {
|
||||
// Sanitize request.
|
||||
parts := strings.Split(url, "/")
|
||||
for i, p := range parts {
|
||||
// Allow last part to be "", ie, for a path to end in /
|
||||
if p == "" {
|
||||
if i != len(parts)-1 {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// net/http sanitizes this anyway, but we better be sure.
|
||||
if p == "." || p == ".." {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
path := "//" + strings.Join(parts, "/")
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func (s *service) handlePageAuto(w http.ResponseWriter, r *http.Request, rev, rpath, dirpath string) {
|
||||
cfg, err := config.ForPath(s.source, dirpath)
|
||||
if err != nil {
|
||||
glog.Errorf("could not get config for path %q: %w", dirpath, err)
|
||||
handle500(w, r)
|
||||
return
|
||||
}
|
||||
for _, f := range cfg.DefaultIndex {
|
||||
fpath := dirpath + f
|
||||
file, err := s.source.IsFile(fpath)
|
||||
if err != nil {
|
||||
glog.Errorf("IsFile(%q): %w", fpath, err)
|
||||
handle500(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if file {
|
||||
s.handleMarkdown(w, r, s.source, rev, fpath, cfg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handle404(w, r)
|
||||
}
|
||||
|
||||
func (s *service) handlePage(w http.ResponseWriter, r *http.Request, rev, page string) {
|
||||
path := urlPathToDepotPath(page)
|
||||
|
||||
if strings.HasSuffix(path, "/") {
|
||||
// Directory path given, autoresolve.
|
||||
dirpath := path
|
||||
if path != "//" {
|
||||
dirpath = strings.TrimSuffix(path, "/") + "/"
|
||||
}
|
||||
s.handlePageAuto(w, r, rev, path, dirpath)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, try loading the file.
|
||||
file, err := s.source.IsFile(path)
|
||||
if err != nil {
|
||||
glog.Errorf("IsFile(%q): %w", path, err)
|
||||
handle500(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// File exists, render that.
|
||||
if file {
|
||||
parts := strings.Split(path, "/")
|
||||
dirpath := strings.Join(parts[:(len(parts)-1)], "/")
|
||||
cfg, err := config.ForPath(s.source, dirpath)
|
||||
if err != nil {
|
||||
glog.Errorf("could not get config for path %q: %w", dirpath, err)
|
||||
handle500(w, r)
|
||||
return
|
||||
}
|
||||
s.handleMarkdown(w, r, s.source, rev, path, cfg)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise assume directory, try all posibilities.
|
||||
dirpath := path
|
||||
if path != "//" {
|
||||
dirpath = strings.TrimSuffix(path, "/") + "/"
|
||||
}
|
||||
s.handlePageAuto(w, r, rev, path, dirpath)
|
||||
}
|
49
devtools/hackdoc/markdown.go
Normal file
49
devtools/hackdoc/markdown.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.hackerspace.pl/hscloud/devtools/hackdoc/config"
|
||||
"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"gopkg.in/russross/blackfriday.v2"
|
||||
)
|
||||
|
||||
func (s *service) handleMarkdown(w http.ResponseWriter, r *http.Request, src source.Source, branch, path string, cfg *config.Config) {
|
||||
data, err := src.ReadFile(path)
|
||||
if err != nil {
|
||||
glog.Errorf("ReadFile(%q): %w", err)
|
||||
handle500(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
rendered := blackfriday.Run([]byte(data))
|
||||
|
||||
logRequest(w, r, "serving markdown at %s, cfg %+v", path, cfg)
|
||||
|
||||
// TODO(q3k): allow markdown files to override which template to load
|
||||
tmpl, ok := cfg.Templates["default"]
|
||||
if !ok {
|
||||
glog.Errorf("No default template found for %s", path)
|
||||
// TODO(q3k): implement fallback template
|
||||
w.Write(rendered)
|
||||
return
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"Rendered": template.HTML(rendered),
|
||||
"Title": path,
|
||||
"Path": path,
|
||||
"PathInDepot": strings.TrimPrefix(path, "//"),
|
||||
"HackdocURL": flagHackdocURL,
|
||||
"GitwebURL": fmt.Sprintf(flagGitwebURLPattern, flagGitwebDefaultBranch, strings.TrimPrefix(path, "//")),
|
||||
}
|
||||
err = tmpl.Execute(w, vars)
|
||||
if err != nil {
|
||||
glog.Errorf("Could not execute template for %s: %v", err)
|
||||
}
|
||||
}
|
12
devtools/hackdoc/source/BUILD.bazel
Normal file
12
devtools/hackdoc/source/BUILD.bazel
Normal file
|
@ -0,0 +1,12 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"source.go",
|
||||
"source_local.go",
|
||||
],
|
||||
importpath = "code.hackerspace.pl/hscloud/devtools/hackdoc/source",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["@com_github_golang_glog//:go_default_library"],
|
||||
)
|
10
devtools/hackdoc/source/source.go
Normal file
10
devtools/hackdoc/source/source.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package source
|
||||
|
||||
type Source interface {
|
||||
IsFile(path string) (bool, error)
|
||||
ReadFile(path string) ([]byte, error)
|
||||
IsDirectory(path string) (bool, error)
|
||||
|
||||
CacheSet(dependencies []string, key string, value interface{})
|
||||
CacheGet(key string) interface{}
|
||||
}
|
78
devtools/hackdoc/source/source_local.go
Normal file
78
devtools/hackdoc/source/source_local.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LocalSource struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func NewLocal(root string) Source {
|
||||
return &LocalSource{
|
||||
root: strings.TrimRight(root, "/"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalSource) resolve(path string) (string, error) {
|
||||
if !strings.HasPrefix(path, "//") {
|
||||
return "", fmt.Errorf("invalid path %q, expected // prefix", path)
|
||||
}
|
||||
path = path[2:]
|
||||
if strings.HasPrefix(path, "/") {
|
||||
return "", fmt.Errorf("invalid path %q, expected // prefix", path)
|
||||
}
|
||||
|
||||
return s.root + "/" + path, nil
|
||||
}
|
||||
|
||||
func (s *LocalSource) IsFile(path string) (bool, error) {
|
||||
path, err := s.resolve(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("os.Stat(%q): %w", path, err)
|
||||
}
|
||||
return !stat.IsDir(), nil
|
||||
}
|
||||
|
||||
func (s *LocalSource) ReadFile(path string) ([]byte, error) {
|
||||
path, err := s.resolve(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO(q3k): limit size
|
||||
return ioutil.ReadFile(path)
|
||||
}
|
||||
|
||||
func (s *LocalSource) IsDirectory(path string) (bool, error) {
|
||||
path, err := s.resolve(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("os.Stat(%q): %w", path, err)
|
||||
}
|
||||
return stat.IsDir(), nil
|
||||
}
|
||||
|
||||
func (s *LocalSource) CacheSet(dependencies []string, key string, value interface{}) {
|
||||
// Swallow writes. The local filesystem can always change underneath us and
|
||||
// we're not tracking anything, so we cannot ever keep caches.
|
||||
}
|
||||
|
||||
func (s *LocalSource) CacheGet(key string) interface{} {
|
||||
return nil
|
||||
}
|
11
devtools/hackdoc/tpl/base.html
Normal file
11
devtools/hackdoc/tpl/base.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>hackdoc:{{ .Title }}</title>
|
||||
{{ template "head" . }}
|
||||
</head>
|
||||
<body>
|
||||
{{ template "body" . }}
|
||||
</body>
|
||||
</html>
|
172
devtools/hackdoc/tpl/default.html
Normal file
172
devtools/hackdoc/tpl/default.html
Normal file
|
@ -0,0 +1,172 @@
|
|||
{{ define "head" }}
|
||||
<style type="text/css">
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
line-height: 1.25em;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
max-width: 80em;
|
||||
padding: 1rem 0 1rem 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
background-color: #fefefe;
|
||||
padding: 0.5rem 2rem 3rem 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 1.2em;
|
||||
font-family: Consolas, monospace;
|
||||
margin-top: 1rem;
|
||||
padding: 0.5em 0 0.5em 0;
|
||||
}
|
||||
|
||||
.header a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.header a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.header span.red {
|
||||
color: #b30014;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 0.8em;
|
||||
color: #ccc;
|
||||
font-weight: 800;
|
||||
font-family: helvetica, arial, sans-serif;
|
||||
padding: 0.5em 1em 1em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4 {
|
||||
font-family: helvetica, arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
padding: 1em 0 0 0;
|
||||
font-weight: 800;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.3em;
|
||||
padding: 0.8em 0 0 0;
|
||||
color: #333;
|
||||
font-weight: 800;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
padding: 0.4em 0 0 0;
|
||||
color: #444;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.0em;
|
||||
color: #555;
|
||||
}
|
||||
code {
|
||||
font-family: Consolas, monospace;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
pre {
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #d8d8d8;
|
||||
margin: 1em;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
}
|
||||
p {
|
||||
margin-top: 0.8em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
ul li::before {
|
||||
content: "•";
|
||||
color: #333;;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-left: -1em;
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
{{ define "body" }}
|
||||
<div class="wrapper">
|
||||
<div class="column">
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<span class="red">hackdoc:</span><span>{{ .Path }}</span> <a href="{{ .GitwebURL }}">[git]</a>
|
||||
</div>
|
||||
{{ .Rendered }}
|
||||
</div>
|
||||
<div class="footer">
|
||||
Generated by <a href="{{ .HackdocURL }}/devtools/hackdoc">hackdoc</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
7
hackdoc.toml
Normal file
7
hackdoc.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
default_index = ["index.md", "readme.md", "README.md"]
|
||||
|
||||
[template.default]
|
||||
sources = [
|
||||
"//devtools/hackdoc/tpl/base.html",
|
||||
"//devtools/hackdoc/tpl/default.html",
|
||||
]
|
Loading…
Reference in a new issue