commit ca3ea1900d1e5356d765f6dae575108c08d736c4 Author: yanweidong Date: Sun Sep 7 17:26:39 2025 +0800 feat diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fe6919 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# protoc-gen-markdown + +## install + +```bash +go install git.apinb.com/bsm-tools/protoc-gen-markdown +``` + +## generate markdown + +```bash +protoc --markdown_out=Mhello.proto=./:. ./hello.proto +# set path prefix to /api +protoc --markdown_out=Mhello.proto=./,prefix=/api:. ./hello.proto +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..388390a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.apinb.com/bsm-tools/protoc-gen-markdown + +go 1.24 + +require ( + github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c + google.golang.org/protobuf v1.27.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..df96974 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9GH0RoeVZQKzFJcTLoAixx5s5Gq3pTIS+n354= +github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/hello.md b/hello.md new file mode 100644 index 0000000..619ef62 --- /dev/null +++ b/hello.md @@ -0,0 +1,130 @@ +# Demo + + Service demo. + + All leading comments will be copied to markdown. + +- [/api/demo.Demo/Echo1](#apidemodemoecho1) +- [/api/demo.Demo/Echo2](#apidemodemoecho2) + +## /api/demo.Demo/Echo1 + + Rpc demo + + All leading comments will be copied to markdown. + + +### Request +```javascript +{ + // boolean value demo + a: false, // type + // 32 bit int value demo + b: 0, // type + // 64 bit int value demo + c: "0", // type, stored as string + // float value demo + d: 0.0, // type + // string value demo + e: "", // type + // bytes value demo + f: "", // type, stored as base64 string + // message value demo + g: { + // string list value demo + a: [""], // list + // map value demo + b: { + "0": "" + }, // map + // self reference value demo + c: { + a: {}, // type, self-referenced message will be displayed as {} + }, // type + // enum value demo + d: "Unknown", // enum + // message list value demo + e: [{ + name: "", // type + age: 0, // type + }], // list + }, // type + // imported message value demo + h: { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + seconds: "0", // type + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + nanos: 0, // type + }, // type +} +``` + +### Reply +```javascript +{} +``` +## /api/demo.Demo/Echo2 + + Another rpc demo + + +### Request +```javascript +{} +``` + +### Reply +```javascript +{ + // boolean value demo + a: false, // type + // 32 bit int value demo + b: 0, // type + // 64 bit int value demo + c: "0", // type, stored as string + // float value demo + d: 0.0, // type + // string value demo + e: "", // type + // bytes value demo + f: "", // type, stored as base64 string + // message value demo + g: { + // string list value demo + a: [""], // list + // map value demo + b: { + "0": "" + }, // map + // self reference value demo + c: { + a: {}, // type, self-referenced message will be displayed as {} + }, // type + // enum value demo + d: "Unknown", // enum + // message list value demo + e: [{ + name: "", // type + age: 0, // type + }], // list + }, // type + // imported message value demo + h: { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + seconds: "0", // type + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + nanos: 0, // type + }, // type +} +``` + diff --git a/hello.proto b/hello.proto new file mode 100644 index 0000000..2b77a81 --- /dev/null +++ b/hello.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package demo; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +// Service demo. +// +// All leading comments will be copied to markdown. +service Demo { + // Rpc demo + // + // All leading comments will be copied to markdown. + rpc Echo1(Foo) returns (google.protobuf.Empty) {} + // Another rpc demo + rpc Echo2(google.protobuf.Empty) returns (Foo) {} +} + +// Leading comments of message will be ignored. +message Foo { + // boolean value demo + bool a = 1; + // 32 bit int value demo + int32 b = 2; + // 64 bit int value demo + int64 c = 3; // stored as string + // float value demo + double d = 4; + // string value demo + string e = 5; + // bytes value demo + bytes f = 6; // stored as base64 string + // message value demo + Bar g = 7; + // imported message value demo + google.protobuf.Timestamp h = 8; +} + +message Bar { + // string list value demo + repeated string a = 1; + // map value demo + map b = 2; + // self reference value demo + Baz c = 3; + enum Sex { + Unknown = 0; + Male = 1; + Female = 2; + } + // enum value demo + Sex d = 4; + // message list value demo + repeated Person e = 5; +} + +message Baz { + Bar a = 1; // self-referenced message will be displayed as {} +} + +message Person { + string name = 1; + int32 age = 2; +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e78609b --- /dev/null +++ b/main.go @@ -0,0 +1,207 @@ +package main + +import ( + "flag" + "fmt" + "strings" + + "github.com/ditashi/jsbeautifier-go/jsbeautifier" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +func main() { + g := markdown{} + + var flags flag.FlagSet + + flags.StringVar(&g.Prefix, "prefix", "/", "API path prefix") + + protogen.Options{ + ParamFunc: flags.Set, + }.Run(g.Generate) +} + +type markdown struct { + Prefix string + + msgs []protoreflect.FullName +} + +func (md *markdown) in(m *protogen.Message) { + md.msgs = append(md.msgs, m.Desc.FullName()) + +} + +func (md *markdown) out() { + md.msgs = md.msgs[0 : len(md.msgs)-1] +} + +func (md *markdown) recursive(m *protogen.Message) bool { + for _, n := range md.msgs { + if n == m.Desc.FullName() { + return true + } + } + return false +} + +func (md *markdown) Generate(plugin *protogen.Plugin) error { + // The service should be defined in the last file. + // All other files are imported by the service proto. + for _, f := range plugin.Files { + if len(f.Services) == 0 { + return nil + } + + fname := f.GeneratedFilenamePrefix + ".md" + t := plugin.NewGeneratedFile(fname, f.GoImportPath) + + for _, s := range f.Services { + t.P("# ", s.Desc.Name()) + t.P() + t.P(string(s.Comments.Leading)) + + for _, m := range s.Methods { + name := string(m.Desc.FullName()) + api := md.api(name) + anchor := md.anchor(api) + + t.P(fmt.Sprintf("- [%s](#%s)", api, anchor)) + } + t.P() + for _, m := range s.Methods { + n := string(m.Desc.FullName()) + t.P("## ", md.api(n)) + t.P() + t.P(string(m.Comments.Leading)) + t.P() + t.P("### Request") + t.P("```javascript") + t.P(md.jsDocForMessage(m.Input)) + t.P("```") + t.P() + t.P("### Reply") + t.P("```javascript") + t.P(md.jsDocForMessage(m.Output)) + t.P("```") + } + } + + t.P() + } + + return nil +} + +func (md *markdown) api(s string) string { + i := strings.LastIndex(s, ".") + + prefix := strings.Trim(md.Prefix, "/") + if prefix != "" { + prefix = "/" + prefix + } + + return prefix + "/" + s[:i] + "/" + s[i+1:] +} + +func (md *markdown) anchor(s string) string { + s = strings.ToLower(s) + s = strings.ReplaceAll(s, ".", "") + s = strings.ReplaceAll(s, "/", "") + return s +} + +func (md *markdown) scalarDefaultValue(field *protogen.Field) string { + switch field.Desc.Kind() { + case protoreflect.StringKind, protoreflect.BytesKind: + return `""` + case protoreflect.Fixed64Kind, protoreflect.Int64Kind, + protoreflect.Sfixed64Kind, protoreflect.Sint64Kind, + protoreflect.Uint64Kind: + return `"0"` + case protoreflect.DoubleKind, protoreflect.FloatKind: + return `0.0` + case protoreflect.BoolKind: + return "false" + default: + return "0" + } +} + +func (md *markdown) jsDocForField(field *protogen.Field) string { + js := field.Comments.Leading.String() + js += string(field.Desc.Name()) + ":" + + var vv string + var vt string + if field.Desc.IsMap() { + vf := field.Message.Fields[1] + if m := vf.Message; m != nil { + vv = md.jsDocForMessage(m) + vt = string(vf.Message.Desc.FullName()) + } else { + vv = md.scalarDefaultValue(vf) + vt = vf.Desc.Kind().String() + } + kf := field.Desc.MapKey() + vv = fmt.Sprintf("{\n\"%s\":%s}", kf.Default().String(), vv) + vt = fmt.Sprintf("%s,%s", kf.Kind().String(), vt) + } else if field.Message != nil { + if md.recursive(field.Message) { + vv = "{}" + } else { + vv = md.jsDocForMessage(field.Message) + } + vt = string(field.Message.Desc.Name()) + } else if field.Enum != nil { + vv = `"` + string(field.Enum.Values[0].Desc.Name()) + `"` + vt = "" + for i, v := range field.Enum.Values { + if i > 0 { + vt += "," + } + vt += string(v.Desc.Name()) + } + } else if field.Oneof != nil { + vv = `"Does Not Support OneOf"` + } else { + vv = md.scalarDefaultValue(field) + vt = field.Desc.Kind().String() + } + + if field.Desc.IsList() { + js += fmt.Sprintf("[%s], // list<%s>", vv, vt) + } else if field.Desc.IsMap() { + js += vv + fmt.Sprintf(", // map<%s>", vt) + } else if field.Enum != nil { + js += vv + fmt.Sprintf(", // enum<%s>", vt) + } else { + js += vv + fmt.Sprintf(", // type<%s>", vt) + } + + if t := string(field.Comments.Trailing); len(t) > 0 { + js += ", " + strings.TrimLeft(t, " ") + } else { + js += "\n" + } + + return js +} + +func (md *markdown) jsDocForMessage(m *protogen.Message) string { + md.in(m) + defer md.out() + + js := "{\n" + + for _, field := range m.Fields { + js += md.jsDocForField(field) + } + + js += "}" + options := jsbeautifier.DefaultOptions() + js, _ = jsbeautifier.Beautify(&js, options) + + return js +}