docstring.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package api
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "html"
  7. "os"
  8. "regexp"
  9. "strings"
  10. )
  11. type apiDocumentation struct {
  12. *API
  13. Operations map[string]string
  14. Service string
  15. Shapes map[string]shapeDocumentation
  16. }
  17. type shapeDocumentation struct {
  18. Base string
  19. Refs map[string]string
  20. }
  21. // AttachDocs attaches documentation from a JSON filename.
  22. func (a *API) AttachDocs(filename string) {
  23. d := apiDocumentation{API: a}
  24. f, err := os.Open(filename)
  25. defer f.Close()
  26. if err != nil {
  27. panic(err)
  28. }
  29. err = json.NewDecoder(f).Decode(&d)
  30. if err != nil {
  31. panic(err)
  32. }
  33. d.setup()
  34. }
  35. func (d *apiDocumentation) setup() {
  36. d.API.Documentation = docstring(d.Service)
  37. if d.Service == "" {
  38. d.API.Documentation =
  39. fmt.Sprintf("// %s is a client for %s.\n", d.API.StructName(), d.API.NiceName())
  40. }
  41. for op, doc := range d.Operations {
  42. d.API.Operations[op].Documentation = docstring(doc)
  43. }
  44. for shape, info := range d.Shapes {
  45. if sh := d.API.Shapes[shape]; sh != nil {
  46. sh.Documentation = docstring(info.Base)
  47. }
  48. for ref, doc := range info.Refs {
  49. if doc == "" {
  50. continue
  51. }
  52. parts := strings.Split(ref, "$")
  53. if sh := d.API.Shapes[parts[0]]; sh != nil {
  54. if m := sh.MemberRefs[parts[1]]; m != nil {
  55. m.Documentation = docstring(doc)
  56. }
  57. }
  58. }
  59. }
  60. }
  61. var reNewline = regexp.MustCompile(`\r?\n`)
  62. var reMultiSpace = regexp.MustCompile(`\s+`)
  63. var reComments = regexp.MustCompile(`<!--.*?-->`)
  64. var reFullname = regexp.MustCompile(`\s*<fullname?>.+?<\/fullname?>\s*`)
  65. var reExamples = regexp.MustCompile(`<examples?>.+?<\/examples?>`)
  66. var rePara = regexp.MustCompile(`<(?:p|h\d)>(.+?)</(?:p|h\d)>`)
  67. var reLink = regexp.MustCompile(`<a href="(.+?)">(.+?)</a>`)
  68. var reTag = regexp.MustCompile(`<.+?>`)
  69. var reEndNL = regexp.MustCompile(`\n+$`)
  70. // docstring rewrites a string to insert godocs formatting.
  71. func docstring(doc string) string {
  72. doc = reNewline.ReplaceAllString(doc, "")
  73. doc = reMultiSpace.ReplaceAllString(doc, " ")
  74. doc = reComments.ReplaceAllString(doc, "")
  75. doc = reFullname.ReplaceAllString(doc, "")
  76. doc = reExamples.ReplaceAllString(doc, "")
  77. doc = rePara.ReplaceAllString(doc, "$1\n\n")
  78. doc = reLink.ReplaceAllString(doc, "$2 ($1)")
  79. doc = reTag.ReplaceAllString(doc, "$1")
  80. doc = reEndNL.ReplaceAllString(doc, "")
  81. doc = strings.TrimSpace(doc)
  82. if doc == "" {
  83. return "\n"
  84. }
  85. doc = html.UnescapeString(doc)
  86. doc = wrap(doc, 72)
  87. return commentify(doc)
  88. }
  89. // commentify converts a string to a Go comment
  90. func commentify(doc string) string {
  91. lines := strings.Split(doc, "\n")
  92. out := []string{}
  93. for i, line := range lines {
  94. if i > 0 && line == "" && lines[i-1] == "" {
  95. continue
  96. }
  97. out = append(out, "// "+line)
  98. }
  99. return strings.Join(out, "\n") + "\n"
  100. }
  101. // wrap returns a rewritten version of text to have line breaks
  102. // at approximately length characters. Line breaks will only be
  103. // inserted into whitespace.
  104. func wrap(text string, length int) string {
  105. var buf bytes.Buffer
  106. var last rune
  107. var lastNL bool
  108. var col int
  109. for _, c := range text {
  110. switch c {
  111. case '\r': // ignore this
  112. continue // and also don't track `last`
  113. case '\n': // ignore this too, but reset col
  114. if col >= length || last == '\n' {
  115. buf.WriteString("\n\n")
  116. }
  117. col = 0
  118. case ' ', '\t': // opportunity to split
  119. if col >= length {
  120. buf.WriteByte('\n')
  121. col = 0
  122. } else {
  123. if !lastNL {
  124. buf.WriteRune(c)
  125. }
  126. col++ // count column
  127. }
  128. default:
  129. buf.WriteRune(c)
  130. col++
  131. }
  132. lastNL = c == '\n'
  133. last = c
  134. }
  135. return buf.String()
  136. }