diff --git a/go.mod b/go.mod index 72a2890..1a7aab0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/sh0rez/docsonnet go 1.14 require ( - github.com/Masterminds/sprig/v3 v3.0.2 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.4.0 + github.com/stretchr/testify v1.4.0 ) diff --git a/go.sum b/go.sum index 05d96a7..bb32130 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,13 @@ -github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14= -github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.0.2 h1:wz22D0CiSctrliXiI9ZO3HoNApweeRGftyDN+BQa3B8= -github.com/Masterminds/sprig/v3 v3.0.2/go.mod h1:oesJ8kPONMONaZgtiHNzUShJbksypC5kWczhZAf6+aU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= -github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/load.libsonnet b/load.libsonnet index 1b3763f..8165a06 100644 --- a/load.libsonnet +++ b/load.libsonnet @@ -54,5 +54,4 @@ then reshaped { api: $.fillObjects(reshaped.api) } else reshaped; self.clean(filled), - // reshaped, } diff --git a/main.go b/main.go index d1f1f2b..2cd2367 100644 --- a/main.go +++ b/main.go @@ -3,17 +3,19 @@ package main import ( "encoding/json" "errors" + "fmt" "io/ioutil" "log" "os" ) -type Doc struct { +type Package struct { Name string `json:"name"` Import string `json:"import"` Help string `json:"help"` API Fields `json:"api"` + Sub map[string]Package } func main() { @@ -22,14 +24,12 @@ func main() { log.Fatalln(err) } - var d Doc + var d Package if err := json.Unmarshal(data, &d); err != nil { log.Fatalln(err) } - if _, err := render(d); err != nil { - log.Fatalln(err) - } + fmt.Println(render(d)) } type Object struct { diff --git a/markdown.go b/markdown.go new file mode 100644 index 0000000..20be568 --- /dev/null +++ b/markdown.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + "sort" + "strings" + + "github.com/sh0rez/docsonnet/pkg/md" + "github.com/sh0rez/docsonnet/pkg/slug" +) + +func render(pkg Package) string { + // head + elems := []md.Elem{ + md.Headline(1, "package "+pkg.Name), + md.CodeBlock("jsonnet", fmt.Sprintf(`local %s = import "%s"`, pkg.Name, pkg.Import)), + md.Text(pkg.Help), + } + + // index + elems = append(elems, + md.Headline(2, "Index"), + md.List(mdIndex(pkg.API, "", slug.New())...), + ) + + // api + elems = append(elems, md.Headline(2, "Fields")) + elems = append(elems, mdApi(pkg.API, "")...) + + return md.Doc(elems...).String() +} + +func mdIndex(api Fields, path string, s *slug.Slugger) []md.Elem { + var elems []md.Elem + for _, k := range sortFields(api) { + v := api[k] + switch { + case v.Function != nil: + fn := v.Function + name := md.Text(fmt.Sprintf("fn %s(%s)", fn.Name, params(fn.Args))) + link := "#" + s.Slug("fn "+path+fn.Name) + elems = append(elems, md.Link(md.Code(name), link)) + case v.Object != nil: + obj := v.Object + name := md.Text("obj " + path + obj.Name) + link := "#" + s.Slug("obj "+path+obj.Name) + elems = append(elems, md.Link(md.Code(name), link)) + elems = append(elems, md.List(mdIndex(obj.Fields, path+obj.Name+".", s)...)) + } + } + return elems +} + +func mdApi(api Fields, path string) []md.Elem { + var elems []md.Elem + + for _, k := range sortFields(api) { + v := api[k] + switch { + case v.Function != nil: + fn := v.Function + elems = append(elems, + md.Headline(3, fmt.Sprintf("fn %s%s", path, fn.Name)), + md.CodeBlock("ts", fmt.Sprintf("%s(%s)", fn.Name, params(fn.Args))), + md.Text(fn.Help), + ) + case v.Object != nil: + obj := v.Object + elems = append(elems, + md.Headline(2, fmt.Sprintf("obj %s%s", path, obj.Name)), + md.Text(obj.Help), + ) + elems = append(elems, mdApi(obj.Fields, path+obj.Name+".")...) + } + } + + return elems +} + +func sortFields(api Fields) []string { + keys := make([]string, len(api)) + for k := range api { + keys = append(keys, k) + } + + sort.Slice(keys, func(i, j int) bool { + iK, jK := keys[i], keys[j] + if api[iK].Function != nil && api[jK].Function == nil { + return true + } + if api[iK].Function == nil && api[jK].Function != nil { + return false + } + return iK < jK + }) + + return keys +} + +func params(a []Argument) string { + args := make([]string, 0, len(a)) + for _, a := range a { + arg := a.Name + if a.Default != nil { + arg = fmt.Sprintf("%s=%v", arg, a.Default) + } + args = append(args, arg) + } + + return strings.Join(args, ", ") +} diff --git a/pkg/md/md.go b/pkg/md/md.go new file mode 100644 index 0000000..8afb980 --- /dev/null +++ b/pkg/md/md.go @@ -0,0 +1,130 @@ +package md + +import ( + "fmt" + "strings" +) + +type Elem interface { + String() string +} + +type JoinType struct { + elems []Elem + with string +} + +func (p JoinType) String() string { + s := "" + for _, e := range p.elems { + s += p.with + e.String() + } + return strings.TrimPrefix(s, p.with) +} + +func Paragraph(elems ...Elem) JoinType { + return JoinType{elems: elems, with: " "} +} + +func Doc(elems ...Elem) JoinType { + return JoinType{elems: elems, with: "\n\n"} +} + +type TextType struct { + content string +} + +func (t TextType) String() string { + return t.content +} + +func Text(text string) TextType { + return TextType{content: text} +} + +type HeadlineType struct { + level int + content string +} + +func (h HeadlineType) String() string { + return strings.Repeat("#", h.level) + " " + h.content +} + +func Headline(level int, content string) HeadlineType { + return HeadlineType{ + level: level, + content: content, + } +} + +type SurroundType struct { + body Elem + surround string +} + +func (s SurroundType) String() string { + return s.surround + s.body.String() + s.surround +} + +func Bold(e Elem) SurroundType { + return SurroundType{body: e, surround: "**"} +} + +func Italic(e Elem) SurroundType { + return SurroundType{body: e, surround: "*"} +} + +func Code(e Elem) SurroundType { + return SurroundType{body: e, surround: "`"} +} + +type CodeBlockType struct { + lang string + snippet string +} + +func (c CodeBlockType) String() string { + return fmt.Sprintf("```%s\n%s\n```", c.lang, c.snippet) +} + +func CodeBlock(lang, snippet string) CodeBlockType { + return CodeBlockType{lang: lang, snippet: snippet} +} + +type ListType struct { + elems []Elem +} + +func (l ListType) String() string { + s := "" + for _, e := range l.elems { + switch t := e.(type) { + case ListType: + s += "\n " + strings.Join(strings.Split(t.String(), "\n"), "\n ") + default: + s += "\n* " + t.String() + } + } + return strings.TrimPrefix(s, "\n") +} + +func List(elems ...Elem) ListType { + return ListType{elems: elems} +} + +type LinkType struct { + desc Elem + href string +} + +func (l LinkType) String() string { + return fmt.Sprintf("[%s](%s)", l.desc.String(), l.href) +} + +func Link(desc Elem, href string) LinkType { + return LinkType{ + desc: desc, + href: href, + } +} diff --git a/pkg/md/md_test.go b/pkg/md/md_test.go new file mode 100644 index 0000000..db230b9 --- /dev/null +++ b/pkg/md/md_test.go @@ -0,0 +1,25 @@ +package md + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestList(t *testing.T) { + l := List( + Text("foo"), + Text("bar"), + List( + Text("baz"), + Text("bing"), + ), + Text("boing"), + ).String() + + assert.Equal(t, `* foo +* bar + * baz + * bing +* boing`, l) +} diff --git a/pkg/slug/slug.go b/pkg/slug/slug.go new file mode 100644 index 0000000..879daf1 --- /dev/null +++ b/pkg/slug/slug.go @@ -0,0 +1,35 @@ +package slug + +import ( + "regexp" + "strconv" + "strings" +) + +type Slugger struct { + occurences map[string]int +} + +var ( + expWhitespace = regexp.MustCompile(`\s`) + expSpecials = regexp.MustCompile("[\u2000-\u206F\u2E00-\u2E7F\\'!\"#$%&()*+,./:;<=>?@[\\]^`{|}~’]") +) + +func New() *Slugger { + return &Slugger{ + occurences: make(map[string]int), + } +} + +func (s *Slugger) Slug(str string) string { + str = expWhitespace.ReplaceAllString(str, "-") + str = expSpecials.ReplaceAllString(str, "") + + old := str + if o := s.occurences[str]; o > 0 { + str += "-" + strconv.Itoa(o) + } + s.occurences[old] = s.occurences[old] + 1 + + return strings.ToLower(str) +} diff --git a/pkg/slug/slug_test.go b/pkg/slug/slug_test.go new file mode 100644 index 0000000..a5cffbd --- /dev/null +++ b/pkg/slug/slug_test.go @@ -0,0 +1,44 @@ +package slug + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSlug(t *testing.T) { + + cases := [][]struct { + in, out string + }{ + { + {"foo", "foo"}, + {"foo", "foo-1"}, + {"foo bar", "foo-bar"}, + }, + { + {"foo", "foo"}, + {"fooCamelCase", "foocamelcase"}, + }, + { + {"foo", "foo"}, + {"foo", "foo-1"}, + // {"foo 1", "foo-1-1"}, // these are too rare for Jsonnet + // {"foo 1", "foo-1-2"}, + {"foo", "foo-2"}, + }, + { + {"heading with a - dash", "heading-with-a---dash"}, + {"heading with an _ underscore", "heading-with-an-_-underscore"}, + {"heading with a period.txt", "heading-with-a-periodtxt"}, + {"exchange.bind_headers(exchange, routing [, bindCallback])", "exchangebind_headersexchange-routing--bindcallback"}, + }, + } + + for _, cs := range cases { + s := New() + for _, c := range cs { + assert.Equal(t, c.out, s.Slug(c.in)) + } + } +} diff --git a/render.go b/render.go deleted file mode 100644 index 40c0956..0000000 --- a/render.go +++ /dev/null @@ -1,165 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "os" - "strings" - "text/template" - - "github.com/Masterminds/sprig/v3" -) - -var indexTmpl = strings.Replace(` -# package {{.Name}} -´´´jsonnet -local {{.Name}} = import "{{.Import}}" -´´´ - -{{.Help}} - -## Index - -{{ range .Index }}{{ $l := mul .Level 2}}{{repeat (int $l) " "}}* ´{{ .Line }}´ -{{ end }} - -{{ range .Fields }} {{.Render}} -{{ end }} - -`, "´", "`", -1) - -type Renderable interface { - Render() string -} - -var objTmpl = strings.Replace(` -## {{ .Name }} -{{ .Help }} -`, "´", "`", -1) - -type obj struct { - Object -} - -func (o obj) Render() string { - t := template.Must(template.New("").Parse(objTmpl)) - var buf bytes.Buffer - if err := t.Execute(&buf, o); err != nil { - panic(err) - } - return buf.String() -} - -type fn struct { - Function -} - -func (f fn) Params() string { - args := make([]string, 0, len(f.Args)) - for _, a := range f.Args { - arg := a.Name - if a.Default != nil { - arg = fmt.Sprint("%s=%v", arg, a.Default) - } - args = append(args, arg) - } - - return strings.Join(args, ", ") -} - -func (f fn) Signature() string { - return fmt.Sprintf("%s(%s)", f.Name, f.Params()) -} - -var fnTmpl = strings.Replace(` -### fn {{ .Name }} -´´´ -{{ .Signature }} -´´´ -{{ .Help }} -`, "´", "`", -1) - -func (f fn) Render() string { - t := template.Must(template.New("").Parse(fnTmpl)) - var buf bytes.Buffer - if err := t.Execute(&buf, f); err != nil { - panic(err) - } - return buf.String() -} - -func render(d Doc) (string, error) { - tmpl := template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse(indexTmpl)) - if err := tmpl.Execute(os.Stdout, &doc{ - Name: d.Name, - Import: d.Import, - Help: d.Help, - Index: buildIndex(d.API, 0), - Fields: renderables(d.API, ""), - }); err != nil { - return "", err - } - - return "", nil -} - -type doc struct { - Name string - Import string - Help string - - Index []indexElem - Fields []Renderable -} - -type indexElem struct { - Line string - Level int -} - -func renderables(fields map[string]Field, prefix string) []Renderable { - rs := []Renderable{} - for _, f := range fields { - switch { - case f.Function != nil: - fnc := fn{*f.Function} - fnc.Name = strings.TrimPrefix(prefix+"."+fnc.Name, ".") - rs = append(rs, fnc) - case f.Object != nil: - o := obj{*f.Object} - o.Name = strings.TrimPrefix(prefix+"."+o.Name, ".") - rs = append(rs, o) - - childs := renderables(o.Fields, o.Name) - rs = append(rs, childs...) - } - } - return rs -} - -func buildIndex(fields map[string]Field, level int) []indexElem { - elems := []indexElem{} - for _, f := range fields { - line := indexLine(f) - elems = append(elems, indexElem{ - Line: line, - Level: level, - }) - - if f.Object != nil { - childs := buildIndex(f.Object.Fields, level+1) - elems = append(elems, childs...) - } - } - return elems -} - -func indexLine(f Field) string { - switch { - case f.Function != nil: - return "fn " + fn{*f.Function}.Signature() - case f.Object != nil: - return fmt.Sprintf("obj %s", f.Object.Name) - } - panic("wtf") -}