docstring.go 9.2 KB

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