mirror of
https://gerrit.hackerspace.pl/hscloud
synced 2024-10-18 05:07:46 +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.
|
# Invoke go_rules_dependencies depending on host platform.
|
||||||
load("//tools:go_sdk.bzl", "gen_imports")
|
load("//tools:go_sdk.bzl", "gen_imports")
|
||||||
|
|
||||||
gen_imports(name = "go_sdk_imports")
|
gen_imports(name = "go_sdk_imports")
|
||||||
|
|
||||||
load("@go_sdk_imports//:imports.bzl", "load_go_sdk")
|
load("@go_sdk_imports//:imports.bzl", "load_go_sdk")
|
||||||
|
|
||||||
load_go_sdk()
|
load_go_sdk()
|
||||||
|
|
||||||
# Go Gazelle rules
|
# Go Gazelle rules
|
||||||
|
@ -1948,3 +1951,15 @@ go_repository(
|
||||||
commit = "d07dcb9293789fdc99c797d3499a5799bc343b86",
|
commit = "d07dcb9293789fdc99c797d3499a5799bc343b86",
|
||||||
importpath = "gopkg.in/irc.v3",
|
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