docstring.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. // +build codegen
  2. package api
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "html"
  8. "os"
  9. "regexp"
  10. "strings"
  11. xhtml "golang.org/x/net/html"
  12. )
  13. type apiDocumentation struct {
  14. *API
  15. Operations map[string]string
  16. Service string
  17. Shapes map[string]shapeDocumentation
  18. }
  19. type shapeDocumentation struct {
  20. Base string
  21. Refs map[string]string
  22. }
  23. // AttachDocs attaches documentation from a JSON filename.
  24. func (a *API) AttachDocs(filename string) {
  25. d := apiDocumentation{API: a}
  26. f, err := os.Open(filename)
  27. defer f.Close()
  28. if err != nil {
  29. panic(err)
  30. }
  31. err = json.NewDecoder(f).Decode(&d)
  32. if err != nil {
  33. panic(err)
  34. }
  35. d.setup()
  36. }
  37. func (d *apiDocumentation) setup() {
  38. d.API.Documentation = docstring(d.Service)
  39. if d.Service == "" {
  40. d.API.Documentation =
  41. fmt.Sprintf("// %s is a client for %s.\n", d.API.StructName(), d.API.NiceName())
  42. }
  43. for op, doc := range d.Operations {
  44. d.API.Operations[op].Documentation = strings.TrimSpace(docstring(doc))
  45. }
  46. for shape, info := range d.Shapes {
  47. if sh := d.API.Shapes[shape]; sh != nil {
  48. sh.Documentation = docstring(info.Base)
  49. }
  50. for ref, doc := range info.Refs {
  51. if doc == "" {
  52. continue
  53. }
  54. parts := strings.Split(ref, "$")
  55. if sh := d.API.Shapes[parts[0]]; sh != nil {
  56. if m := sh.MemberRefs[parts[1]]; m != nil {
  57. m.Documentation = docstring(doc)
  58. }
  59. }
  60. }
  61. }
  62. }
  63. var reNewline = regexp.MustCompile(`\r?\n`)
  64. var reMultiSpace = regexp.MustCompile(`\s+`)
  65. var reComments = regexp.MustCompile(`<!--.*?-->`)
  66. var reFullname = regexp.MustCompile(`\s*<fullname?>.+?<\/fullname?>\s*`)
  67. var reExamples = regexp.MustCompile(`<examples?>.+?<\/examples?>`)
  68. var reEndNL = regexp.MustCompile(`\n+$`)
  69. // docstring rewrites a string to insert godocs formatting.
  70. func docstring(doc string) string {
  71. doc = reNewline.ReplaceAllString(doc, "")
  72. doc = reMultiSpace.ReplaceAllString(doc, " ")
  73. doc = reComments.ReplaceAllString(doc, "")
  74. doc = reFullname.ReplaceAllString(doc, "")
  75. doc = reExamples.ReplaceAllString(doc, "")
  76. doc = generateDoc(doc)
  77. doc = reEndNL.ReplaceAllString(doc, "")
  78. if doc == "" {
  79. return "\n"
  80. }
  81. doc = html.UnescapeString(doc)
  82. return commentify(doc)
  83. }
  84. const (
  85. indent = " "
  86. )
  87. // style is what we want to prefix a string with.
  88. // For instance, <li>Foo</li><li>Bar</li>, will generate
  89. // * Foo
  90. // * Bar
  91. var style = map[string]string{
  92. "ul": indent + "* ",
  93. "li": indent + "* ",
  94. "code": indent,
  95. "pre": indent,
  96. }
  97. // commentify converts a string to a Go comment
  98. func commentify(doc string) string {
  99. lines := strings.Split(doc, "\n")
  100. out := []string{}
  101. for i, line := range lines {
  102. if i > 0 && line == "" && lines[i-1] == "" {
  103. continue
  104. }
  105. out = append(out, "// "+line)
  106. }
  107. return strings.Join(out, "\n") + "\n"
  108. }
  109. // wrap returns a rewritten version of text to have line breaks
  110. // at approximately length characters. Line breaks will only be
  111. // inserted into whitespace.
  112. func wrap(text string, length int, isIndented bool) string {
  113. var buf bytes.Buffer
  114. var last rune
  115. var lastNL bool
  116. var col int
  117. for _, c := range text {
  118. switch c {
  119. case '\r': // ignore this
  120. continue // and also don't track `last`
  121. case '\n': // ignore this too, but reset col
  122. if col >= length || last == '\n' {
  123. buf.WriteString("\n")
  124. }
  125. buf.WriteString("\n")
  126. col = 0
  127. case ' ', '\t': // opportunity to split
  128. if col >= length {
  129. buf.WriteByte('\n')
  130. col = 0
  131. if isIndented {
  132. buf.WriteString(indent)
  133. col += 3
  134. }
  135. } else {
  136. // We only want to write a leading space if the col is greater than zero.
  137. // This will provide the proper spacing for documentation.
  138. buf.WriteRune(c)
  139. col++ // count column
  140. }
  141. default:
  142. buf.WriteRune(c)
  143. col++
  144. }
  145. lastNL = c == '\n'
  146. _ = lastNL
  147. last = c
  148. }
  149. return buf.String()
  150. }
  151. type tagInfo struct {
  152. tag string
  153. key string
  154. val string
  155. txt string
  156. raw string
  157. closingTag bool
  158. }
  159. // generateDoc will generate the proper doc string for html encoded or plain text doc entries.
  160. func generateDoc(htmlSrc string) string {
  161. tokenizer := xhtml.NewTokenizer(strings.NewReader(htmlSrc))
  162. tokens := buildTokenArray(tokenizer)
  163. scopes := findScopes(tokens)
  164. return walk(scopes)
  165. }
  166. func buildTokenArray(tokenizer *xhtml.Tokenizer) []tagInfo {
  167. tokens := []tagInfo{}
  168. for tt := tokenizer.Next(); tt != xhtml.ErrorToken; tt = tokenizer.Next() {
  169. switch tt {
  170. case xhtml.TextToken:
  171. txt := string(tokenizer.Text())
  172. if len(tokens) == 0 {
  173. info := tagInfo{
  174. raw: txt,
  175. }
  176. tokens = append(tokens, info)
  177. }
  178. tn, _ := tokenizer.TagName()
  179. key, val, _ := tokenizer.TagAttr()
  180. info := tagInfo{
  181. tag: string(tn),
  182. key: string(key),
  183. val: string(val),
  184. txt: txt,
  185. }
  186. tokens = append(tokens, info)
  187. case xhtml.StartTagToken:
  188. tn, _ := tokenizer.TagName()
  189. key, val, _ := tokenizer.TagAttr()
  190. info := tagInfo{
  191. tag: string(tn),
  192. key: string(key),
  193. val: string(val),
  194. }
  195. tokens = append(tokens, info)
  196. case xhtml.SelfClosingTagToken, xhtml.EndTagToken:
  197. tn, _ := tokenizer.TagName()
  198. key, val, _ := tokenizer.TagAttr()
  199. info := tagInfo{
  200. tag: string(tn),
  201. key: string(key),
  202. val: string(val),
  203. closingTag: true,
  204. }
  205. tokens = append(tokens, info)
  206. }
  207. }
  208. return tokens
  209. }
  210. // walk is used to traverse each scoped block. These scoped
  211. // blocks will act as blocked text where we do most of our
  212. // text manipulation.
  213. func walk(scopes [][]tagInfo) string {
  214. doc := ""
  215. // Documentation will be chunked by scopes.
  216. // Meaning, for each scope will be divided by one or more newlines.
  217. for _, scope := range scopes {
  218. indentStr, isIndented := priorityIndentation(scope)
  219. block := ""
  220. href := ""
  221. after := false
  222. level := 0
  223. lastTag := ""
  224. for _, token := range scope {
  225. if token.closingTag {
  226. endl := closeTag(token, level)
  227. block += endl
  228. level--
  229. lastTag = ""
  230. } else if token.txt == "" {
  231. if token.val != "" {
  232. href, after = formatText(token, "")
  233. }
  234. if level == 1 && isIndented {
  235. block += indentStr
  236. }
  237. level++
  238. lastTag = token.tag
  239. } else {
  240. if token.txt != " " {
  241. str, _ := formatText(token, lastTag)
  242. block += str
  243. if after {
  244. block += href
  245. after = false
  246. }
  247. } else {
  248. fmt.Println(token.tag)
  249. str, _ := formatText(tagInfo{}, lastTag)
  250. block += str
  251. }
  252. }
  253. }
  254. if !isIndented {
  255. block = strings.TrimPrefix(block, " ")
  256. }
  257. block = wrap(block, 72, isIndented)
  258. doc += block
  259. }
  260. return doc
  261. }
  262. // closeTag will divide up the blocks of documentation to be formated properly.
  263. func closeTag(token tagInfo, level int) string {
  264. switch token.tag {
  265. case "pre", "li", "div":
  266. return "\n"
  267. case "p", "h1", "h2", "h3", "h4", "h5", "h6":
  268. return "\n\n"
  269. case "code":
  270. // indented code is only at the 0th level.
  271. if level == 0 {
  272. return "\n"
  273. }
  274. }
  275. return ""
  276. }
  277. // formatText will format any sort of text based off of a tag. It will also return
  278. // a boolean to add the string after the text token.
  279. func formatText(token tagInfo, lastTag string) (string, bool) {
  280. switch token.tag {
  281. case "a":
  282. if token.val != "" {
  283. return fmt.Sprintf(" (%s)", token.val), true
  284. }
  285. }
  286. // We don't care about a single space nor no text.
  287. if len(token.txt) == 0 || token.txt == " " {
  288. return "", false
  289. }
  290. // Here we want to indent code blocks that are newlines
  291. if lastTag == "code" {
  292. // Greater than one, because we don't care about newlines in the beginning
  293. block := ""
  294. if lines := strings.Split(token.txt, "\n"); len(lines) > 1 {
  295. for _, line := range lines {
  296. block += indent + line
  297. }
  298. block += "\n"
  299. return block, false
  300. }
  301. }
  302. return token.txt, false
  303. }
  304. // This is a parser to check what type of indention is needed.
  305. func priorityIndentation(blocks []tagInfo) (string, bool) {
  306. if len(blocks) == 0 {
  307. return "", false
  308. }
  309. v, ok := style[blocks[0].tag]
  310. return v, ok
  311. }
  312. // Divides into scopes based off levels.
  313. // For instance,
  314. // <p>Testing<code>123</code></p><ul><li>Foo</li></ul>
  315. // This has 2 scopes, the <p> and <ul>
  316. func findScopes(tokens []tagInfo) [][]tagInfo {
  317. level := 0
  318. scope := []tagInfo{}
  319. scopes := [][]tagInfo{}
  320. for _, token := range tokens {
  321. // we will clear empty tagged tokens from the array
  322. txt := strings.TrimSpace(token.txt)
  323. tag := strings.TrimSpace(token.tag)
  324. if len(txt) == 0 && len(tag) == 0 {
  325. continue
  326. }
  327. scope = append(scope, token)
  328. // If it is a closing tag then we check what level
  329. // we are on. If it is 0, then that means we have found a
  330. // scoped block.
  331. if token.closingTag {
  332. level--
  333. if level == 0 {
  334. scopes = append(scopes, scope)
  335. scope = []tagInfo{}
  336. }
  337. // Check opening tags and increment the level
  338. } else if token.txt == "" {
  339. level++
  340. }
  341. }
  342. // In this case, we did not run into a closing tag. This would mean
  343. // we have plaintext for documentation.
  344. if len(scopes) == 0 {
  345. scopes = append(scopes, scope)
  346. }
  347. return scopes
  348. }