1
0
Fork 0
mirror of https://github.com/imjasonh/kontain.me synced 2026-07-21 06:27:06 +00:00

implement viz.kontain.me

This commit is contained in:
Jason Hall 2019-05-17 21:57:28 -04:00
parent a92a026666
commit 72da24e6d2
25 changed files with 1982 additions and 1 deletions

View file

@ -1,4 +1,5 @@
baseImageOverrides: baseImageOverrides:
github.com/imjasonh/kontain.me/cmd/ko: golang github.com/imjasonh/kontain.me/cmd/ko: golang
github.com/imjasonh/kontain.me/cmd/buildpack: packs/samples:v3alpha2 github.com/imjasonh/kontain.me/cmd/buildpack: packs/samples:v3alpha2
github.com/imjasonh/kontain.me/cmd/api: packs/samples:v3alpha2 github.com/imjasonh/kontain.me/cmd/api: heroku/buildpacks
github.com/imjasonh/kontain.me/cmd/viz: gcr.io/kontainme/graphviz

18
Gopkg.lock generated
View file

@ -19,6 +19,14 @@
revision = "8c41231e01b2085512d98153bcffb847ff9b4b9f" revision = "8c41231e01b2085512d98153bcffb847ff9b4b9f"
version = "v0.38.0" version = "v0.38.0"
[[projects]]
digest = "1:6f9339c912bbdda81302633ad7e99a28dfa5a639c864061f1929510a9a64aa74"
name = "github.com/dustin/go-humanize"
packages = ["."]
pruneopts = "UT"
revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e"
version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:499b09c6fecbec76e839ef9acf07af100219048ed22b3ca7d78948ae2767e781" digest = "1:499b09c6fecbec76e839ef9acf07af100219048ed22b3ca7d78948ae2767e781"
name = "github.com/golang/protobuf" name = "github.com/golang/protobuf"
@ -90,6 +98,14 @@
revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c" revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c"
version = "v0.5.1" version = "v0.5.1"
[[projects]]
branch = "master"
digest = "1:b2a3ee6c6584219e30393fe61a85e9a10a7d9f966b7d66e9fedeb5ee5642195e"
name = "github.com/tmc/dot"
packages = ["."]
pruneopts = "UT"
revision = "6d252d5ff882ba9963502860f30ab3673c6c7322"
[[projects]] [[projects]]
digest = "1:429596ab6fea904f2ea2a74de9c95692e39a5a6269e778b770676374e7063f3e" digest = "1:429596ab6fea904f2ea2a74de9c95692e39a5a6269e778b770676374e7063f3e"
name = "go.opencensus.io" name = "go.opencensus.io"
@ -295,6 +311,7 @@
"cloud.google.com/go/compute/metadata", "cloud.google.com/go/compute/metadata",
"cloud.google.com/go/datastore", "cloud.google.com/go/datastore",
"cloud.google.com/go/storage", "cloud.google.com/go/storage",
"github.com/dustin/go-humanize",
"github.com/google/go-containerregistry/pkg/authn", "github.com/google/go-containerregistry/pkg/authn",
"github.com/google/go-containerregistry/pkg/name", "github.com/google/go-containerregistry/pkg/name",
"github.com/google/go-containerregistry/pkg/v1", "github.com/google/go-containerregistry/pkg/v1",
@ -303,6 +320,7 @@
"github.com/google/go-containerregistry/pkg/v1/types", "github.com/google/go-containerregistry/pkg/v1/types",
"github.com/google/ko/pkg/build", "github.com/google/ko/pkg/build",
"github.com/google/uuid", "github.com/google/uuid",
"github.com/tmc/dot",
"golang.org/x/oauth2", "golang.org/x/oauth2",
"google.golang.org/api/cloudbuild/v1", "google.golang.org/api/cloudbuild/v1",
"google.golang.org/api/googleapi", "google.golang.org/api/googleapi",

6
cmd/viz/Dockerfile Normal file
View file

@ -0,0 +1,6 @@
FROM alpine
RUN apk add --update --no-cache \
ca-certificates \
graphviz \
ttf-freefont

BIN
cmd/viz/kodata/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

41
cmd/viz/kodata/index.html Normal file
View file

@ -0,0 +1,41 @@
<html>
<head>
<style>
body { font-family: Arial; }
textarea { width: 100%; height: 80%; font-size: 10pt; font-family: monospace; }
input[type=submit] { width: 100%; }
</style>
</head>
<body>
<h1>Specify image references to generate layer hierarchy</h1>
<p>Lines beggining with <code>#</code> are ignored.</p>
<form action="/viz" method="POST" id="form">
<textarea name="images" form="form">
# Some OS images
ubuntu
ubuntu:18.04
ubuntu:18.10
ubuntu:19.04
alpine
alpine:3.9.4
debian
centos
# Some Knative Serving images
gcr.io/knative-releases/github.com/knative/serving/cmd/activator:v0.6.0
gcr.io/knative-releases/github.com/knative/serving/cmd/autoscaler
gcr.io/knative-releases/github.com/knative/serving/cmd/controller
gcr.io/knative-releases/github.com/knative/serving/cmd/queue
gcr.io/knative-releases/github.com/knative/serving/cmd/webhook
# Some GCB builder images
gcr.io/cloud-builders/docker
gcr.io/cloud-builders/gcloud
gcr.io/cloud-builders/gcloud-slim
gcr.io/cloud-builders/kubectl
gcr.io/cloud-builders/gsutil
</textarea><br />
<input type="submit"></input>
</form>
</body>
</html>

173
cmd/viz/main.go Normal file
View file

@ -0,0 +1,173 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"strings"
humanize "github.com/dustin/go-humanize"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/tmc/dot"
)
func main() {
http.Handle("/", http.FileServer(http.Dir("/var/run/ko")))
http.Handle("/viz", &server{
info: log.New(os.Stdout, "I ", log.Ldate|log.Ltime|log.Lshortfile),
error: log.New(os.Stderr, "E ", log.Ldate|log.Ltime|log.Lshortfile),
})
log.Println("Starting...")
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
type server struct {
info, error *log.Logger
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.info.Println("handler:", r.Method, r.URL)
if r.Method != http.MethodPost {
http.Error(w, "must be post", http.StatusMethodNotAllowed)
return
}
r.ParseForm()
log.Println(r.Form) // TODO
refs, err := images(r.FormValue("images"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
d, err := genDot(refs)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := graphviz(strings.NewReader(d), w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func images(s string) ([]name.Reference, error) {
log.Printf("in: %q", s) // TODO
lines := strings.Split(s, "\n")
var refs []name.Reference
uniq := map[string]bool{} // dedupe
for i, l := range lines {
l := strings.TrimSpace(l)
if l == "" || strings.HasPrefix(l, "#") {
continue
}
ref, err := name.ParseReference(l)
if err != nil {
return nil, fmt.Errorf("line %d (%q): %v", i, l, err)
}
if !uniq[ref.String()] {
uniq[ref.String()] = true
refs = append(refs, ref)
}
}
return refs, nil
}
func genDot(refs []name.Reference) (string, error) {
g := dot.NewGraph("images")
g.SetType(dot.DIGRAPH)
scratch := dot.NewNode("scratch")
scratch.Set("shape", "octagon")
scratch.Set("style", "filled")
scratch.Set("color", "coral")
g.AddNode(scratch)
edges := map[string]bool{}
for _, ref := range refs {
layers, err := getLayers(ref)
if err != nil {
return "", fmt.Errorf("Failed to get layers for %q, ignoring: %v\n", ref, err)
}
var totalSize uint64
for i, this := range layers {
totalSize += uint64(this.Size)
if i == len(layers)-1 {
continue
}
next := layers[i+1]
k := short(this) + short(next)
if !edges[k] {
edges[k] = true
src := dot.NewNode(short(next))
dst := dot.NewNode(short(this))
e := dot.NewEdge(src, dst)
g.AddNode(src)
g.AddNode(dst)
g.AddEdge(e)
}
}
bottom := short(layers[0])
k := "scratch" + bottom
if !edges[k] {
n := dot.NewNode(bottom)
e := dot.NewEdge(n, scratch)
g.AddNode(n)
g.AddEdge(e)
edges[k] = true
}
lbl := dot.NewNode(fmt.Sprintf("%s\n%s", ref.String(), humanize.Bytes(totalSize)))
lbl.Set("shape", "box")
lbl.Set("style", "filled")
lbl.Set("color", "cornflowerblue")
if strings.Contains(ref.Context().String(), "gcr.io") {
lbl.Set("URL", "https://"+ref.String())
}
g.AddNode(lbl)
top := layers[len(layers)-1]
e := dot.NewEdge(lbl, dot.NewNode(short(top))) // already added
e.Set("style", "dotted")
g.AddEdge(e)
}
return g.String(), nil
}
func short(layer v1.Descriptor) string {
return fmt.Sprintf("%s\n%s", layer.Digest.String()[7:19], humanize.Bytes(uint64(layer.Size)))
}
func getLayers(ref name.Reference) ([]v1.Descriptor, error) {
i, err := remote.Image(ref)
if err != nil {
return nil, fmt.Errorf("remote.Image: %v", err)
}
m, err := i.Manifest()
if err != nil {
return nil, fmt.Errorf("image.Manifest: %v", err)
}
return m.Layers, nil
}
func graphviz(in io.Reader, out io.Writer) error {
cmd := exec.Command("dot", "-Tsvg")
cmd.Stdin = in
cmd.Stdout = out
return cmd.Run()
}

View file

@ -2,6 +2,13 @@
set -euxo pipefail set -euxo pipefail
docker build -t gcr.io/kontainme/graphviz -f cmd/viz/Dockerfile cmd/viz/
docker push gcr.io/kontainme/graphviz
KO_DOCKER_REPO=gcr.io/kontainme ko publish -P ./cmd/viz && \
gcloud --project=kontainme beta run deploy viz \
--image=gcr.io/kontainme/github.com/imjasonh/kontain.me/cmd/viz \
--region=us-central1
KO_DOCKER_REPO=gcr.io/kontainme ko publish -P ./cmd/api && \ KO_DOCKER_REPO=gcr.io/kontainme ko publish -P ./cmd/api && \
gcloud --project=kontainme beta run deploy api \ gcloud --project=kontainme beta run deploy api \
--image=gcr.io/kontainme/github.com/imjasonh/kontain.me/cmd/api \ --image=gcr.io/kontainme/github.com/imjasonh/kontain.me/cmd/api \

21
vendor/github.com/dustin/go-humanize/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,21 @@
sudo: false
language: go
go:
- 1.3.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- master
matrix:
allow_failures:
- go: master
fast_finish: true
install:
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- go tool vet .
- go test -v -race ./...

21
vendor/github.com/dustin/go-humanize/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<http://www.opensource.org/licenses/mit-license.php>

124
vendor/github.com/dustin/go-humanize/README.markdown generated vendored Normal file
View file

@ -0,0 +1,124 @@
# Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize)
Just a few functions for helping humanize times and sizes.
`go get` it as `github.com/dustin/go-humanize`, import it as
`"github.com/dustin/go-humanize"`, use it as `humanize`.
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
complete documentation.
## Sizes
This lets you take numbers like `82854982` and convert them to useful
strings like, `83 MB` or `79 MiB` (whichever you prefer).
Example:
```go
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
```
## Times
This lets you take a `time.Time` and spit it out in relative terms.
For example, `12 seconds ago` or `3 days from now`.
Example:
```go
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
```
Thanks to Kyle Lemons for the time implementation from an IRC
conversation one day. It's pretty neat.
## Ordinals
From a [mailing list discussion][odisc] where a user wanted to be able
to label ordinals.
0 -> 0th
1 -> 1st
2 -> 2nd
3 -> 3rd
4 -> 4th
[...]
Example:
```go
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
```
## Commas
Want to shove commas into numbers? Be my guest.
0 -> 0
100 -> 100
1000 -> 1,000
1000000000 -> 1,000,000,000
-100000 -> -100,000
Example:
```go
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
```
## Ftoa
Nicer float64 formatter that removes trailing zeros.
```go
fmt.Printf("%f", 2.24) // 2.240000
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
fmt.Printf("%f", 2.0) // 2.000000
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
```
## SI notation
Format numbers with [SI notation][sinotation].
Example:
```go
humanize.SI(0.00000000223, "M") // 2.23 nM
```
## English-specific functions
The following functions are in the `humanize/english` subpackage.
### Plurals
Simple English pluralization
```go
english.PluralWord(1, "object", "") // object
english.PluralWord(42, "object", "") // objects
english.PluralWord(2, "bus", "") // buses
english.PluralWord(99, "locus", "loci") // loci
english.Plural(1, "object", "") // 1 object
english.Plural(42, "object", "") // 42 objects
english.Plural(2, "bus", "") // 2 buses
english.Plural(99, "locus", "loci") // 99 loci
```
### Word series
Format comma-separated words lists with conjuctions:
```go
english.WordSeries([]string{"foo"}, "and") // foo
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
```
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix

31
vendor/github.com/dustin/go-humanize/big.go generated vendored Normal file
View file

@ -0,0 +1,31 @@
package humanize
import (
"math/big"
)
// order of magnitude (to a max order)
func oomm(n, b *big.Int, maxmag int) (float64, int) {
mag := 0
m := &big.Int{}
for n.Cmp(b) >= 0 {
n.DivMod(n, b, m)
mag++
if mag == maxmag && maxmag >= 0 {
break
}
}
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
}
// total order of magnitude
// (same as above, but with no upper limit)
func oom(n, b *big.Int) (float64, int) {
mag := 0
m := &big.Int{}
for n.Cmp(b) >= 0 {
n.DivMod(n, b, m)
mag++
}
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
}

173
vendor/github.com/dustin/go-humanize/bigbytes.go generated vendored Normal file
View file

@ -0,0 +1,173 @@
package humanize
import (
"fmt"
"math/big"
"strings"
"unicode"
)
var (
bigIECExp = big.NewInt(1024)
// BigByte is one byte in bit.Ints
BigByte = big.NewInt(1)
// BigKiByte is 1,024 bytes in bit.Ints
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
// BigMiByte is 1,024 k bytes in bit.Ints
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
// BigGiByte is 1,024 m bytes in bit.Ints
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
// BigTiByte is 1,024 g bytes in bit.Ints
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
// BigPiByte is 1,024 t bytes in bit.Ints
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
// BigEiByte is 1,024 p bytes in bit.Ints
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
// BigZiByte is 1,024 e bytes in bit.Ints
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
// BigYiByte is 1,024 z bytes in bit.Ints
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
)
var (
bigSIExp = big.NewInt(1000)
// BigSIByte is one SI byte in big.Ints
BigSIByte = big.NewInt(1)
// BigKByte is 1,000 SI bytes in big.Ints
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
// BigMByte is 1,000 SI k bytes in big.Ints
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
// BigGByte is 1,000 SI m bytes in big.Ints
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
// BigTByte is 1,000 SI g bytes in big.Ints
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
// BigPByte is 1,000 SI t bytes in big.Ints
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
// BigEByte is 1,000 SI p bytes in big.Ints
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
// BigZByte is 1,000 SI e bytes in big.Ints
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
// BigYByte is 1,000 SI z bytes in big.Ints
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
)
var bigBytesSizeTable = map[string]*big.Int{
"b": BigByte,
"kib": BigKiByte,
"kb": BigKByte,
"mib": BigMiByte,
"mb": BigMByte,
"gib": BigGiByte,
"gb": BigGByte,
"tib": BigTiByte,
"tb": BigTByte,
"pib": BigPiByte,
"pb": BigPByte,
"eib": BigEiByte,
"eb": BigEByte,
"zib": BigZiByte,
"zb": BigZByte,
"yib": BigYiByte,
"yb": BigYByte,
// Without suffix
"": BigByte,
"ki": BigKiByte,
"k": BigKByte,
"mi": BigMiByte,
"m": BigMByte,
"gi": BigGiByte,
"g": BigGByte,
"ti": BigTiByte,
"t": BigTByte,
"pi": BigPiByte,
"p": BigPByte,
"ei": BigEiByte,
"e": BigEByte,
"z": BigZByte,
"zi": BigZiByte,
"y": BigYByte,
"yi": BigYiByte,
}
var ten = big.NewInt(10)
func humanateBigBytes(s, base *big.Int, sizes []string) string {
if s.Cmp(ten) < 0 {
return fmt.Sprintf("%d B", s)
}
c := (&big.Int{}).Set(s)
val, mag := oomm(c, base, len(sizes)-1)
suffix := sizes[mag]
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// BigBytes produces a human readable representation of an SI size.
//
// See also: ParseBigBytes.
//
// BigBytes(82854982) -> 83 MB
func BigBytes(s *big.Int) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
return humanateBigBytes(s, bigSIExp, sizes)
}
// BigIBytes produces a human readable representation of an IEC size.
//
// See also: ParseBigBytes.
//
// BigIBytes(82854982) -> 79 MiB
func BigIBytes(s *big.Int) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
return humanateBigBytes(s, bigIECExp, sizes)
}
// ParseBigBytes parses a string representation of bytes into the number
// of bytes it represents.
//
// See also: BigBytes, BigIBytes.
//
// ParseBigBytes("42 MB") -> 42000000, nil
// ParseBigBytes("42 mib") -> 44040192, nil
func ParseBigBytes(s string) (*big.Int, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
val := &big.Rat{}
_, err := fmt.Sscanf(num, "%f", val)
if err != nil {
return nil, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := bigBytesSizeTable[extra]; ok {
mv := (&big.Rat{}).SetInt(m)
val.Mul(val, mv)
rv := &big.Int{}
rv.Div(val.Num(), val.Denom())
return rv, nil
}
return nil, fmt.Errorf("unhandled size name: %v", extra)
}

143
vendor/github.com/dustin/go-humanize/bytes.go generated vendored Normal file
View file

@ -0,0 +1,143 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var bytesSizeTable = map[string]uint64{
"b": Byte,
"kib": KiByte,
"kb": KByte,
"mib": MiByte,
"mb": MByte,
"gib": GiByte,
"gb": GByte,
"tib": TiByte,
"tb": TByte,
"pib": PiByte,
"pb": PByte,
"eib": EiByte,
"eb": EByte,
// Without suffix
"": Byte,
"ki": KiByte,
"k": KByte,
"mi": MiByte,
"m": MByte,
"gi": GiByte,
"g": GByte,
"ti": TiByte,
"t": TByte,
"pi": PiByte,
"p": PByte,
"ei": EiByte,
"e": EByte,
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// Bytes produces a human readable representation of an SI size.
//
// See also: ParseBytes.
//
// Bytes(82854982) -> 83 MB
func Bytes(s uint64) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
return humanateBytes(s, 1000, sizes)
}
// IBytes produces a human readable representation of an IEC size.
//
// See also: ParseBytes.
//
// IBytes(82854982) -> 79 MiB
func IBytes(s uint64) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
return humanateBytes(s, 1024, sizes)
}
// ParseBytes parses a string representation of bytes into the number
// of bytes it represents.
//
// See Also: Bytes, IBytes.
//
// ParseBytes("42 MB") -> 42000000, nil
// ParseBytes("42 mib") -> 44040192, nil
func ParseBytes(s string) (uint64, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := bytesSizeTable[extra]; ok {
f *= float64(m)
if f >= math.MaxUint64 {
return 0, fmt.Errorf("too large: %v", s)
}
return uint64(f), nil
}
return 0, fmt.Errorf("unhandled size name: %v", extra)
}

116
vendor/github.com/dustin/go-humanize/comma.go generated vendored Normal file
View file

@ -0,0 +1,116 @@
package humanize
import (
"bytes"
"math"
"math/big"
"strconv"
"strings"
)
// Comma produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. Comma(834142) -> 834,142
func Comma(v int64) string {
sign := ""
// Min int64 can't be negated to a usable value, so it has to be special cased.
if v == math.MinInt64 {
return "-9,223,372,036,854,775,808"
}
if v < 0 {
sign = "-"
v = 0 - v
}
parts := []string{"", "", "", "", "", "", ""}
j := len(parts) - 1
for v > 999 {
parts[j] = strconv.FormatInt(v%1000, 10)
switch len(parts[j]) {
case 2:
parts[j] = "0" + parts[j]
case 1:
parts[j] = "00" + parts[j]
}
v = v / 1000
j--
}
parts[j] = strconv.Itoa(int(v))
return sign + strings.Join(parts[j:], ",")
}
// Commaf produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. Commaf(834142.32) -> 834,142.32
func Commaf(v float64) string {
buf := &bytes.Buffer{}
if v < 0 {
buf.Write([]byte{'-'})
v = 0 - v
}
comma := []byte{','}
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
pos := 0
if len(parts[0])%3 != 0 {
pos += len(parts[0]) % 3
buf.WriteString(parts[0][:pos])
buf.Write(comma)
}
for ; pos < len(parts[0]); pos += 3 {
buf.WriteString(parts[0][pos : pos+3])
buf.Write(comma)
}
buf.Truncate(buf.Len() - 1)
if len(parts) > 1 {
buf.Write([]byte{'.'})
buf.WriteString(parts[1])
}
return buf.String()
}
// CommafWithDigits works like the Commaf but limits the resulting
// string to the given number of decimal places.
//
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
func CommafWithDigits(f float64, decimals int) string {
return stripTrailingDigits(Commaf(f), decimals)
}
// BigComma produces a string form of the given big.Int in base 10
// with commas after every three orders of magnitude.
func BigComma(b *big.Int) string {
sign := ""
if b.Sign() < 0 {
sign = "-"
b.Abs(b)
}
athousand := big.NewInt(1000)
c := (&big.Int{}).Set(b)
_, m := oom(c, athousand)
parts := make([]string, m+1)
j := len(parts) - 1
mod := &big.Int{}
for b.Cmp(athousand) >= 0 {
b.DivMod(b, athousand, mod)
parts[j] = strconv.FormatInt(mod.Int64(), 10)
switch len(parts[j]) {
case 2:
parts[j] = "0" + parts[j]
case 1:
parts[j] = "00" + parts[j]
}
j--
}
parts[j] = strconv.Itoa(int(b.Int64()))
return sign + strings.Join(parts[j:], ",")
}

40
vendor/github.com/dustin/go-humanize/commaf.go generated vendored Normal file
View file

@ -0,0 +1,40 @@
// +build go1.6
package humanize
import (
"bytes"
"math/big"
"strings"
)
// BigCommaf produces a string form of the given big.Float in base 10
// with commas after every three orders of magnitude.
func BigCommaf(v *big.Float) string {
buf := &bytes.Buffer{}
if v.Sign() < 0 {
buf.Write([]byte{'-'})
v.Abs(v)
}
comma := []byte{','}
parts := strings.Split(v.Text('f', -1), ".")
pos := 0
if len(parts[0])%3 != 0 {
pos += len(parts[0]) % 3
buf.WriteString(parts[0][:pos])
buf.Write(comma)
}
for ; pos < len(parts[0]); pos += 3 {
buf.WriteString(parts[0][pos : pos+3])
buf.Write(comma)
}
buf.Truncate(buf.Len() - 1)
if len(parts) > 1 {
buf.Write([]byte{'.'})
buf.WriteString(parts[1])
}
return buf.String()
}

46
vendor/github.com/dustin/go-humanize/ftoa.go generated vendored Normal file
View file

@ -0,0 +1,46 @@
package humanize
import (
"strconv"
"strings"
)
func stripTrailingZeros(s string) string {
offset := len(s) - 1
for offset > 0 {
if s[offset] == '.' {
offset--
break
}
if s[offset] != '0' {
break
}
offset--
}
return s[:offset+1]
}
func stripTrailingDigits(s string, digits int) string {
if i := strings.Index(s, "."); i >= 0 {
if digits <= 0 {
return s[:i]
}
i++
if i+digits >= len(s) {
return s
}
return s[:i+digits]
}
return s
}
// Ftoa converts a float to a string with no trailing zeros.
func Ftoa(num float64) string {
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
}
// FtoaWithDigits converts a float to a string but limits the resulting string
// to the given number of decimal places, and no trailing zeros.
func FtoaWithDigits(num float64, digits int) string {
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
}

8
vendor/github.com/dustin/go-humanize/humanize.go generated vendored Normal file
View file

@ -0,0 +1,8 @@
/*
Package humanize converts boring ugly numbers to human-friendly strings and back.
Durations can be turned into strings such as "3 days ago", numbers
representing sizes like 82854982 into useful strings like, "83 MB" or
"79 MiB" (whichever you prefer).
*/
package humanize

192
vendor/github.com/dustin/go-humanize/number.go generated vendored Normal file
View file

@ -0,0 +1,192 @@
package humanize
/*
Slightly adapted from the source to fit go-humanize.
Author: https://github.com/gorhill
Source: https://gist.github.com/gorhill/5285193
*/
import (
"math"
"strconv"
)
var (
renderFloatPrecisionMultipliers = [...]float64{
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000,
}
renderFloatPrecisionRounders = [...]float64{
0.5,
0.05,
0.005,
0.0005,
0.00005,
0.000005,
0.0000005,
0.00000005,
0.000000005,
0.0000000005,
}
)
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
// * thousands separator
// * decimal separator
// * decimal precision
//
// Usage: s := RenderFloat(format, n)
// The format parameter tells how to render the number n.
//
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
//
// Examples of format strings, given n = 12345.6789:
// "#,###.##" => "12,345.67"
// "#,###." => "12,345"
// "#,###" => "12345,678"
// "#\u202F###,##" => "12345,68"
// "#.###,###### => 12.345,678900
// "" (aka default format) => 12,345.67
//
// The highest precision allowed is 9 digits after the decimal symbol.
// There is also a version for integer number, FormatInteger(),
// which is convenient for calls within template.
func FormatFloat(format string, n float64) string {
// Special cases:
// NaN = "NaN"
// +Inf = "+Infinity"
// -Inf = "-Infinity"
if math.IsNaN(n) {
return "NaN"
}
if n > math.MaxFloat64 {
return "Infinity"
}
if n < -math.MaxFloat64 {
return "-Infinity"
}
// default format
precision := 2
decimalStr := "."
thousandStr := ","
positiveStr := ""
negativeStr := "-"
if len(format) > 0 {
format := []rune(format)
// If there is an explicit format directive,
// then default values are these:
precision = 9
thousandStr = ""
// collect indices of meaningful formatting directives
formatIndx := []int{}
for i, char := range format {
if char != '#' && char != '0' {
formatIndx = append(formatIndx, i)
}
}
if len(formatIndx) > 0 {
// Directive at index 0:
// Must be a '+'
// Raise an error if not the case
// index: 0123456789
// +0.000,000
// +000,000.0
// +0000.00
// +0000
if formatIndx[0] == 0 {
if format[formatIndx[0]] != '+' {
panic("RenderFloat(): invalid positive sign directive")
}
positiveStr = "+"
formatIndx = formatIndx[1:]
}
// Two directives:
// First is thousands separator
// Raise an error if not followed by 3-digit
// 0123456789
// 0.000,000
// 000,000.00
if len(formatIndx) == 2 {
if (formatIndx[1] - formatIndx[0]) != 4 {
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
}
thousandStr = string(format[formatIndx[0]])
formatIndx = formatIndx[1:]
}
// One directive:
// Directive is decimal separator
// The number of digit-specifier following the separator indicates wanted precision
// 0123456789
// 0.00
// 000,0000
if len(formatIndx) == 1 {
decimalStr = string(format[formatIndx[0]])
precision = len(format) - formatIndx[0] - 1
}
}
}
// generate sign part
var signStr string
if n >= 0.000000001 {
signStr = positiveStr
} else if n <= -0.000000001 {
signStr = negativeStr
n = -n
} else {
signStr = ""
n = 0.0
}
// split number into integer and fractional parts
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
// generate integer part string
intStr := strconv.FormatInt(int64(intf), 10)
// add thousand separator if required
if len(thousandStr) > 0 {
for i := len(intStr); i > 3; {
i -= 3
intStr = intStr[:i] + thousandStr + intStr[i:]
}
}
// no fractional part, we can leave now
if precision == 0 {
return signStr + intStr
}
// generate fractional part
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
// may need padding
if len(fracStr) < precision {
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
}
return signStr + intStr + decimalStr + fracStr
}
// FormatInteger produces a formatted number as string.
// See FormatFloat.
func FormatInteger(format string, n int) string {
return FormatFloat(format, float64(n))
}

25
vendor/github.com/dustin/go-humanize/ordinals.go generated vendored Normal file
View file

@ -0,0 +1,25 @@
package humanize
import "strconv"
// Ordinal gives you the input number in a rank/ordinal format.
//
// Ordinal(3) -> 3rd
func Ordinal(x int) string {
suffix := "th"
switch x % 10 {
case 1:
if x%100 != 11 {
suffix = "st"
}
case 2:
if x%100 != 12 {
suffix = "nd"
}
case 3:
if x%100 != 13 {
suffix = "rd"
}
}
return strconv.Itoa(x) + suffix
}

123
vendor/github.com/dustin/go-humanize/si.go generated vendored Normal file
View file

@ -0,0 +1,123 @@
package humanize
import (
"errors"
"math"
"regexp"
"strconv"
)
var siPrefixTable = map[float64]string{
-24: "y", // yocto
-21: "z", // zepto
-18: "a", // atto
-15: "f", // femto
-12: "p", // pico
-9: "n", // nano
-6: "µ", // micro
-3: "m", // milli
0: "",
3: "k", // kilo
6: "M", // mega
9: "G", // giga
12: "T", // tera
15: "P", // peta
18: "E", // exa
21: "Z", // zetta
24: "Y", // yotta
}
var revSIPrefixTable = revfmap(siPrefixTable)
// revfmap reverses the map and precomputes the power multiplier
func revfmap(in map[float64]string) map[string]float64 {
rv := map[string]float64{}
for k, v := range in {
rv[v] = math.Pow(10, k)
}
return rv
}
var riParseRegex *regexp.Regexp
func init() {
ri := `^([\-0-9.]+)\s?([`
for _, v := range siPrefixTable {
ri += v
}
ri += `]?)(.*)`
riParseRegex = regexp.MustCompile(ri)
}
// ComputeSI finds the most appropriate SI prefix for the given number
// and returns the prefix along with the value adjusted to be within
// that prefix.
//
// See also: SI, ParseSI.
//
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
func ComputeSI(input float64) (float64, string) {
if input == 0 {
return 0, ""
}
mag := math.Abs(input)
exponent := math.Floor(logn(mag, 10))
exponent = math.Floor(exponent/3) * 3
value := mag / math.Pow(10, exponent)
// Handle special case where value is exactly 1000.0
// Should return 1 M instead of 1000 k
if value == 1000.0 {
exponent += 3
value = mag / math.Pow(10, exponent)
}
value = math.Copysign(value, input)
prefix := siPrefixTable[exponent]
return value, prefix
}
// SI returns a string with default formatting.
//
// SI uses Ftoa to format float value, removing trailing zeros.
//
// See also: ComputeSI, ParseSI.
//
// e.g. SI(1000000, "B") -> 1 MB
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
func SI(input float64, unit string) string {
value, prefix := ComputeSI(input)
return Ftoa(value) + " " + prefix + unit
}
// SIWithDigits works like SI but limits the resulting string to the
// given number of decimal places.
//
// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB
// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF
func SIWithDigits(input float64, decimals int, unit string) string {
value, prefix := ComputeSI(input)
return FtoaWithDigits(value, decimals) + " " + prefix + unit
}
var errInvalid = errors.New("invalid input")
// ParseSI parses an SI string back into the number and unit.
//
// See also: SI, ComputeSI.
//
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
func ParseSI(input string) (float64, string, error) {
found := riParseRegex.FindStringSubmatch(input)
if len(found) != 4 {
return 0, "", errInvalid
}
mag := revSIPrefixTable[found[2]]
unit := found[3]
base, err := strconv.ParseFloat(found[1], 64)
return base * mag, unit, err
}

117
vendor/github.com/dustin/go-humanize/times.go generated vendored Normal file
View file

@ -0,0 +1,117 @@
package humanize
import (
"fmt"
"math"
"sort"
"time"
)
// Seconds-based time units
const (
Day = 24 * time.Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
LongTime = 37 * Year
)
// Time formats a time into a relative string.
//
// Time(someT) -> "3 weeks ago"
func Time(then time.Time) string {
return RelTime(then, time.Now(), "ago", "from now")
}
// A RelTimeMagnitude struct contains a relative time point at which
// the relative format of time will switch to a new format string. A
// slice of these in ascending order by their "D" field is passed to
// CustomRelTime to format durations.
//
// The Format field is a string that may contain a "%s" which will be
// replaced with the appropriate signed label (e.g. "ago" or "from
// now") and a "%d" that will be replaced by the quantity.
//
// The DivBy field is the amount of time the time difference must be
// divided by in order to display correctly.
//
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
// DivBy should be time.Minute so whatever the duration is will be
// expressed in minutes.
type RelTimeMagnitude struct {
D time.Duration
Format string
DivBy time.Duration
}
var defaultMagnitudes = []RelTimeMagnitude{
{time.Second, "now", time.Second},
{2 * time.Second, "1 second %s", 1},
{time.Minute, "%d seconds %s", time.Second},
{2 * time.Minute, "1 minute %s", 1},
{time.Hour, "%d minutes %s", time.Minute},
{2 * time.Hour, "1 hour %s", 1},
{Day, "%d hours %s", time.Hour},
{2 * Day, "1 day %s", 1},
{Week, "%d days %s", Day},
{2 * Week, "1 week %s", 1},
{Month, "%d weeks %s", Week},
{2 * Month, "1 month %s", 1},
{Year, "%d months %s", Month},
{18 * Month, "1 year %s", 1},
{2 * Year, "2 years %s", 1},
{LongTime, "%d years %s", Year},
{math.MaxInt64, "a long while %s", 1},
}
// RelTime formats a time into a relative string.
//
// It takes two times and two labels. In addition to the generic time
// delta string (e.g. 5 minutes), the labels are used applied so that
// the label corresponding to the smaller time is applied.
//
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func RelTime(a, b time.Time, albl, blbl string) string {
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
}
// CustomRelTime formats a time into a relative string.
//
// It takes two times two labels and a table of relative time formats.
// In addition to the generic time delta string (e.g. 5 minutes), the
// labels are used applied so that the label corresponding to the
// smaller time is applied.
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
lbl := albl
diff := b.Sub(a)
if a.After(b) {
lbl = blbl
diff = a.Sub(b)
}
n := sort.Search(len(magnitudes), func(i int) bool {
return magnitudes[i].D > diff
})
if n >= len(magnitudes) {
n = len(magnitudes) - 1
}
mag := magnitudes[n]
args := []interface{}{}
escaped := false
for _, ch := range mag.Format {
if escaped {
switch ch {
case 's':
args = append(args, lbl)
case 'd':
args = append(args, diff/mag.DivBy)
}
escaped = false
} else {
escaped = ch == '%'
}
}
return fmt.Sprintf(mag.Format, args...)
}

1
vendor/github.com/tmc/dot/.gitignore generated vendored Normal file
View file

@ -0,0 +1 @@
/dot.test

7
vendor/github.com/tmc/dot/LICENSE.md generated vendored Normal file
View file

@ -0,0 +1,7 @@
Copyright (C) 2012 Travis Cline
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

16
vendor/github.com/tmc/dot/README.md generated vendored Normal file
View file

@ -0,0 +1,16 @@
dot
===
dot language support for Go
See http://godoc.org/github.com/tmc/dot
Todo:
* parser
* regression suite
The pydot library used as a reference
License: MIT

531
vendor/github.com/tmc/dot/dot.go generated vendored Normal file
View file

@ -0,0 +1,531 @@
/*
Package dot implements an API to produce Graphviz dot language output.
Basic Graph creation:
g := dot.NewGraph("G")
g.SetType(dot.DIGRAPH)
...
g.AddEdge(dot.NewNode("A"), dot.NewNode("B"))
...
fmt.Sprint(g)
*/
package dot
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
)
var AttributeError = errors.New("Invalid Attribute")
var graphAttributes = []string{"Damping", "K", "URL", "aspect", "bb", "bgcolor",
"center", "charset", "clusterrank", "colorscheme", "comment", "compound",
"concentrate", "defaultdist", "dim", "dimen", "diredgeconstraints",
"dpi", "epsilon", "esep", "fontcolor", "fontname", "fontnames",
"fontpath", "fontsize", "id", "label", "labeljust", "labelloc",
"landscape", "layers", "layersep", "layout", "levels", "levelsgap",
"lheight", "lp", "lwidth", "margin", "maxiter", "mclimit", "mindist",
"mode", "model", "mosek", "nodesep", "nojustify", "normalize", "nslimit",
"nslimit1", "ordering", "orientation", "outputorder", "overlap",
"overlap_scaling", "pack", "packmode", "pad", "page", "pagedir",
"quadtree", "quantum", "rankdir", "ranksep", "ratio", "remincross",
"repulsiveforce", "resolution", "root", "rotate", "searchsize", "sep",
"showboxes", "size", "smoothing", "sortv", "splines", "start",
"stylesheet", "target", "truecolor", "viewport", "voro_margin",
"rank"}
var edgeAttributes = []string{"URL", "arrowhead", "arrowsize", "arrowtail",
"color", "colorscheme", "comment", "constraint", "decorate", "dir",
"edgeURL", "edgehref", "edgetarget", "edgetooltip", "fontcolor",
"fontname", "fontsize", "headURL", "headclip", "headhref", "headlabel",
"headport", "headtarget", "headtooltip", "href", "id", "label",
"labelURL", "labelangle", "labeldistance", "labelfloat", "labelfontcolor",
"labelfontname", "labelfontsize", "labelhref", "labeltarget",
"labeltooltip", "layer", "len", "lhead", "lp", "ltail", "minlen",
"nojustify", "penwidth", "pos", "samehead", "sametail", "showboxes",
"style", "tailURL", "tailclip", "tailhref", "taillabel", "tailport",
"tailtarget", "tailtooltip", "target", "tooltip", "weight",
// for subgraphs
"rank"}
var nodeAttributes = []string{"URL", "color", "colorscheme", "comment",
"distortion", "fillcolor", "fixedsize", "fontcolor", "fontname",
"fontsize", "group", "height", "id", "image", "imagescale", "label",
"labelloc", "layer", "margin", "nojustify", "orientation", "penwidth",
"peripheries", "pin", "pos", "rects", "regular", "root", "samplepoints",
"shape", "shapefile", "showboxes", "sides", "skew", "sortv", "style",
"target", "tooltip", "vertices", "width", "z",
// The following are attributes dot2tex
"texlbl", "texmode"}
var clusterAttributes = []string{"K", "URL", "bgcolor", "color", "colorscheme",
"fillcolor", "fontcolor", "fontname", "fontsize", "label", "labeljust",
"labelloc", "lheight", "lp", "lwidth", "nojustify", "pencolor",
"penwidth", "peripheries", "sortv", "style", "target", "tooltip"}
var dotKeywords = []string{"graph", "subgraph", "digraph", "node", "edge", "strict"}
type GraphType int
const (
DIGRAPH GraphType = iota
GRAPH
SUBGRAPH
)
// Fields common to all graph object types
type common struct {
_type string
name string
attributes map[string]string
sequence int
parentGraph *Graph
}
type GraphObject interface {
Type() string
Get(string) string
Set(string, string) error
GetParentGraph() *Graph
SetParentGraph(g *Graph)
Sequence() int
}
type graphObjects []GraphObject
func (gol graphObjects) Len() int {
return len(gol)
}
func (gol graphObjects) Less(i, j int) bool {
return gol[i].Sequence() < gol[j].Sequence()
}
func (gol graphObjects) Swap(i, j int) {
gol[i], gol[j] = gol[j], gol[i]
}
type Graph struct {
common
nodeAttributes map[string]string
edgeAttributes map[string]string
sameRank [][]string
strict bool
graphType GraphType
supressDisconnected bool
simplify bool
currentChildSequence int
nodes map[string][]*Node
edges map[string][]*Edge
subgraphs map[string][]*SubGraph
}
func NewGraph(name string) *Graph {
g := &Graph{
common: common{
_type: "graph",
name: name,
attributes: make(map[string]string, 0),
},
nodeAttributes: make(map[string]string),
edgeAttributes: make(map[string]string),
sameRank: make([][]string, 0),
nodes: make(map[string][]*Node, 0),
edges: make(map[string][]*Edge, 0),
subgraphs: make(map[string][]*SubGraph, 0),
currentChildSequence: 1,
}
g.SetParentGraph(g)
return g
}
type SubGraph struct {
Graph
}
func NewSubgraph(name string) *SubGraph {
result := &SubGraph{
Graph: *NewGraph(name),
}
result._type = "subgraph"
result.graphType = SUBGRAPH
return result
}
func indexInSlice(slice []string, toFind string) int {
for i, v := range slice {
if v == toFind {
return i
}
}
return -1
}
var alreadyQuotedRegex = regexp.MustCompile("^\".+\"$")
var validIdentifierRegexWithPort = regexp.MustCompile("^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$")
var validIdentifierRegex = regexp.MustCompile("^[_a-zA-Z][a-zA-Z0-9_,]*$")
func needsQuotes(s string) bool {
if indexInSlice(dotKeywords, s) != -1 {
return false
}
if alreadyQuotedRegex.MatchString(s) {
return false
}
if validIdentifierRegexWithPort.MatchString(s) || validIdentifierRegex.MatchString(s) {
return false
}
return true
}
func QuoteIfNecessary(s string) (result string) {
if needsQuotes(s) {
s = strings.Replace(s, "\"", "\\\"", -1)
s = strings.Replace(s, "\n", "\\n", -1)
s = strings.Replace(s, "\r", "\\r", -1)
s = "\"" + s + "\""
}
return s
}
func validAttribute(attributeCollection []string, attributeName string) bool {
return indexInSlice(attributeCollection, attributeName) != -1
}
func validGraphAttribute(attributeName string) bool {
return validAttribute(graphAttributes, attributeName)
}
func validNodeAttribute(attributeName string) bool {
return validAttribute(nodeAttributes, attributeName)
}
func sortedKeys(sourceMap map[string]string) []string {
keys := make([]string, 0, len(sourceMap))
for k, _ := range sourceMap {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
////////////////////////////////////////////////////////////////////////////////////////
func (gt GraphType) String() string {
if gt == DIGRAPH {
return "digraph"
} else if gt == GRAPH {
return "graph"
} else if gt == SUBGRAPH {
return "subgraph"
}
return "(invalid)"
}
func (c *common) Type() string {
return c._type
}
func (c *common) GetParentGraph() *Graph {
return c.parentGraph
}
func (c *common) SetParentGraph(g *Graph) {
c.parentGraph = g
}
func (c *common) Sequence() int {
return c.sequence
}
func (c *common) Get(attributeName string) string {
return c.attributes[attributeName]
}
func (c *common) Set(attributeName, attributeValue string) error {
c.attributes[attributeName] = attributeValue
return nil
}
func setAttribute(validAttributes []string, attributes map[string]string, attributeName, attributeValue string) error {
if validAttribute(validAttributes, attributeName) {
attributes[attributeName] = attributeValue
return nil
}
return AttributeError
}
func (g *Graph) Set(attributeName, attributeValue string) error {
return setAttribute(graphAttributes, g.common.attributes, attributeName, attributeValue)
}
func (g *Graph) SetGlobalNodeAttr(attributeName, attributeValue string) error {
return setAttribute(nodeAttributes, g.nodeAttributes, attributeName, attributeValue)
}
func (g *Graph) SetGlobalEdgeAttr(attributeName, attributeValue string) error {
return setAttribute(edgeAttributes, g.edgeAttributes, attributeName, attributeValue)
}
func (n *Node) Set(attributeName, attributeValue string) error {
return setAttribute(nodeAttributes, n.common.attributes, attributeName, attributeValue)
}
func (e *Edge) Set(attributeName, attributeValue string) error {
return setAttribute(edgeAttributes, e.common.attributes, attributeName, attributeValue)
}
func (c *common) setSequence(sequence int) {
c.sequence = sequence
}
// SameRank enforces alignment of the given nodes
func (g *Graph) SameRank(nodes []string) {
g.sameRank = append(g.sameRank, nodes)
}
// Set the type of the graph, valid values are GRAPH or DIGRAPH
func (g *Graph) SetType(t GraphType) {
g.graphType = t
// @todo consider disallowing setting type to SUBGRAPH
}
func (c common) Name() string {
return c.name
}
func (g *Graph) GetRoot() (result *Graph) {
result = g
for parent := g.GetParentGraph(); parent != result; parent = parent.GetParentGraph() {
result = parent
}
return result
}
func (g *Graph) getNextSequenceNumber() (next int) {
next = g.currentChildSequence
g.currentChildSequence += 1
return
}
func (g *Graph) AddNode(n *Node) {
name := n.Name()
if _, ok := g.nodes[name]; !ok {
g.nodes[name] = make([]*Node, 0)
}
n.setSequence(g.getNextSequenceNumber())
n.SetParentGraph(g.GetParentGraph())
g.nodes[name] = append(g.nodes[name], n)
}
func (g *Graph) AddEdge(e *Edge) {
name := e.Name()
if _, ok := g.edges[name]; !ok {
g.edges[name] = make([]*Edge, 0)
}
e.setSequence(g.getNextSequenceNumber())
e.SetParentGraph(g.GetParentGraph())
g.edges[name] = append(g.edges[name], e)
}
func (g *Graph) AddSubgraph(sg *SubGraph) {
name := sg.Name()
if _, ok := g.subgraphs[name]; !ok {
g.subgraphs[name] = make([]*SubGraph, 0)
}
sg.setSequence(g.getNextSequenceNumber())
g.subgraphs[name] = append(g.subgraphs[name], sg)
}
func (g *Graph) GetSubgraphs() (result []*SubGraph) {
result = make([]*SubGraph, 0)
for _, sgs := range g.subgraphs {
for _, sg := range sgs {
result = append(result, sg)
}
}
return result
}
func (g Graph) String() string {
var parts []string
if g.strict {
parts = append(parts, "strict ")
}
if g.name == "" {
parts = append(parts, "{\n")
} else {
parts = append(parts, fmt.Sprintf("%s %s {\n", g.graphType, QuoteIfNecessary(g.name)))
}
if len(g.attributes) > 0 {
attrs := make([]string, 0, len(g.attributes))
for _, key := range sortedKeys(g.attributes) {
attrs = append(attrs, " "+key+"="+QuoteIfNecessary(g.attributes[key]))
}
if len(attrs) > 0 {
parts = append(parts, "graph [\n")
parts = append(parts, strings.Join(attrs, ";\n"))
parts = append(parts, ";\n];\n")
}
}
if len(g.nodeAttributes) > 0 {
attrs := make([]string, 0, len(g.nodeAttributes))
for _, key := range sortedKeys(g.nodeAttributes) {
attrs = append(attrs, " "+key+"="+QuoteIfNecessary(g.nodeAttributes[key]))
}
if len(attrs) > 0 {
parts = append(parts, "node [\n")
parts = append(parts, strings.Join(attrs, ";\n"))
parts = append(parts, ";\n];\n")
}
}
if len(g.edgeAttributes) > 0 {
attrs := make([]string, 0, len(g.edgeAttributes))
for _, key := range sortedKeys(g.edgeAttributes) {
attrs = append(attrs, " "+key+"="+QuoteIfNecessary(g.edgeAttributes[key]))
}
if len(attrs) > 0 {
parts = append(parts, "edge [\n")
parts = append(parts, strings.Join(attrs, ";\n"))
parts = append(parts, ";\n];\n")
}
}
objectList := make(graphObjects, 0)
for _, nodes := range g.nodes {
for _, node := range nodes {
objectList = append(objectList, node)
}
}
for _, edges := range g.edges {
for _, edge := range edges {
objectList = append(objectList, edge)
}
}
for _, subgraphs := range g.subgraphs {
for _, subgraph := range subgraphs {
objectList = append(objectList, subgraph)
}
}
sort.Sort(objectList)
for _, obj := range objectList {
//@todo type-based decision making re: supressDisconnected and simplify
//switch o := obj.(type) {
//case *Node:
//}
parts = append(parts, fmt.Sprintf("%s\n", obj))
}
for _, nodes := range g.sameRank {
parts = append(parts, fmt.Sprintf("{ rank=same %s }", strings.Join(nodes, " ")))
}
parts = append(parts, "}\n")
return strings.Join(parts, "")
}
type Node struct {
common
}
func NewNode(name string) *Node {
return &Node{
common{
name: name,
attributes: make(map[string]string, 0),
},
}
}
func (n Node) String() string {
name := QuoteIfNecessary(n.name)
parts := make([]string, 0)
attrs := make([]string, 0)
for _, key := range sortedKeys(n.attributes) {
value := n.attributes[key]
if key == "label" && len(value) > 4 && value[0] == '<' && value[len(value)-1] == '>' {
attrs = append(attrs, key+"="+value)
} else {
attrs = append(attrs, key+"="+QuoteIfNecessary(value))
}
}
if len(attrs) > 0 {
parts = append(parts, strings.Join(attrs, ", "))
}
//@todo don't print if node is empty
if len(parts) > 0 {
name += " [" + strings.Join(parts, ", ") + "]"
}
return name + ";"
}
type Edge struct {
common
points [2]*Node
}
func NewEdge(src, dst *Node) *Edge {
return &Edge{
common{
_type: "edge",
attributes: make(map[string]string, 0),
},
[2]*Node{src, dst},
}
}
func (e Edge) Source() *Node {
return e.points[0]
}
func (e Edge) Destination() *Node {
return e.points[1]
}
func (e Edge) String() string {
src, dst := e.Source(), e.Destination()
parts := make([]string, 0)
parts = append(parts, QuoteIfNecessary(src.Name()))
parent := e.GetParentGraph()
if parent != nil && parent.GetRoot() != nil && parent.GetRoot().graphType == DIGRAPH {
parts = append(parts, "->")
} else {
parts = append(parts, "--")
}
parts = append(parts, QuoteIfNecessary(dst.Name()))
attrs := make([]string, 0)
for _, key := range sortedKeys(e.attributes) {
attrs = append(attrs, key+"="+QuoteIfNecessary(e.attributes[key]))
}
if len(attrs) > 0 {
parts = append(parts, " [")
parts = append(parts, strings.Join(attrs, ", "))
parts = append(parts, "]")
}
return strings.Join(parts, " ")
}
func init() {
sort.Strings(graphAttributes)
sort.Strings(nodeAttributes)
sort.Strings(edgeAttributes)
sort.Strings(clusterAttributes)
}