util.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /*
  2. Copyright 2015 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package main
  14. import (
  15. "fmt"
  16. "path"
  17. "path/filepath"
  18. "regexp"
  19. "strings"
  20. "unicode"
  21. )
  22. // Replaces the text between matching "beginMark" and "endMark" within the
  23. // document represented by "lines" with "insertThis".
  24. //
  25. // Delimiters should occupy own line.
  26. // Returns copy of document with modifications.
  27. func updateMacroBlock(mlines mungeLines, token string, insertThis mungeLines) (mungeLines, error) {
  28. beginMark := beginMungeTag(token)
  29. endMark := endMungeTag(token)
  30. var out mungeLines
  31. betweenBeginAndEnd := false
  32. for _, mline := range mlines {
  33. if mline.preformatted && !betweenBeginAndEnd {
  34. out = append(out, mline)
  35. continue
  36. }
  37. line := mline.data
  38. if mline.beginTag && line == beginMark {
  39. if betweenBeginAndEnd {
  40. return nil, fmt.Errorf("found second begin mark while updating macro blocks")
  41. }
  42. betweenBeginAndEnd = true
  43. out = append(out, mline)
  44. } else if mline.endTag && line == endMark {
  45. if !betweenBeginAndEnd {
  46. return nil, fmt.Errorf("found end mark without begin mark while updating macro blocks")
  47. }
  48. betweenBeginAndEnd = false
  49. out = append(out, insertThis...)
  50. out = append(out, mline)
  51. } else {
  52. if !betweenBeginAndEnd {
  53. out = append(out, mline)
  54. }
  55. }
  56. }
  57. if betweenBeginAndEnd {
  58. return nil, fmt.Errorf("never found closing end mark while updating macro blocks")
  59. }
  60. return out, nil
  61. }
  62. // Tests that a document, represented as a slice of lines, has a line. Ignores
  63. // leading and trailing space.
  64. func hasLine(lines mungeLines, needle string) bool {
  65. for _, mline := range lines {
  66. haystack := strings.TrimSpace(mline.data)
  67. if haystack == needle {
  68. return true
  69. }
  70. }
  71. return false
  72. }
  73. func removeMacroBlock(token string, mlines mungeLines) (mungeLines, error) {
  74. beginMark := beginMungeTag(token)
  75. endMark := endMungeTag(token)
  76. var out mungeLines
  77. betweenBeginAndEnd := false
  78. for _, mline := range mlines {
  79. if mline.preformatted {
  80. out = append(out, mline)
  81. continue
  82. }
  83. line := mline.data
  84. if mline.beginTag && line == beginMark {
  85. if betweenBeginAndEnd {
  86. return nil, fmt.Errorf("found second begin mark while updating macro blocks")
  87. }
  88. betweenBeginAndEnd = true
  89. } else if mline.endTag && line == endMark {
  90. if !betweenBeginAndEnd {
  91. return nil, fmt.Errorf("found end mark without begin mark while updating macro blocks")
  92. }
  93. betweenBeginAndEnd = false
  94. } else {
  95. if !betweenBeginAndEnd {
  96. out = append(out, mline)
  97. }
  98. }
  99. }
  100. if betweenBeginAndEnd {
  101. return nil, fmt.Errorf("never found closing end mark while updating macro blocks")
  102. }
  103. return out, nil
  104. }
  105. // Add a macro block to the beginning of a set of lines
  106. func prependMacroBlock(token string, mlines mungeLines) mungeLines {
  107. beginLine := newMungeLine(beginMungeTag(token))
  108. endLine := newMungeLine(endMungeTag(token))
  109. out := mungeLines{beginLine, endLine}
  110. if len(mlines) > 0 && mlines[0].data != "" {
  111. out = append(out, blankMungeLine)
  112. }
  113. return append(out, mlines...)
  114. }
  115. // Add a macro block to the end of a set of lines
  116. func appendMacroBlock(mlines mungeLines, token string) mungeLines {
  117. beginLine := newMungeLine(beginMungeTag(token))
  118. endLine := newMungeLine(endMungeTag(token))
  119. out := mlines
  120. if len(mlines) > 0 && mlines[len(mlines)-1].data != "" {
  121. out = append(out, blankMungeLine)
  122. }
  123. return append(out, beginLine, endLine)
  124. }
  125. // Tests that a document, represented as a slice of lines, has a macro block.
  126. func hasMacroBlock(lines mungeLines, token string) bool {
  127. beginMark := beginMungeTag(token)
  128. endMark := endMungeTag(token)
  129. foundBegin := false
  130. for _, mline := range lines {
  131. if mline.preformatted {
  132. continue
  133. }
  134. if !mline.beginTag && !mline.endTag {
  135. continue
  136. }
  137. line := mline.data
  138. switch {
  139. case !foundBegin && line == beginMark:
  140. foundBegin = true
  141. case foundBegin && line == endMark:
  142. return true
  143. }
  144. }
  145. return false
  146. }
  147. // Returns the canonical begin-tag for a given description. This does not
  148. // include the trailing newline.
  149. func beginMungeTag(desc string) string {
  150. return fmt.Sprintf("<!-- BEGIN MUNGE: %s -->", desc)
  151. }
  152. // Returns the canonical end-tag for a given description. This does not
  153. // include the trailing newline.
  154. func endMungeTag(desc string) string {
  155. return fmt.Sprintf("<!-- END MUNGE: %s -->", desc)
  156. }
  157. type mungeLine struct {
  158. data string
  159. preformatted bool
  160. header bool
  161. link bool
  162. beginTag bool
  163. endTag bool
  164. }
  165. type mungeLines []mungeLine
  166. func (m1 mungeLines) Equal(m2 mungeLines) bool {
  167. if len(m1) != len(m2) {
  168. return false
  169. }
  170. for i := range m1 {
  171. if m1[i].data != m2[i].data {
  172. return false
  173. }
  174. }
  175. return true
  176. }
  177. func (mlines mungeLines) String() string {
  178. slice := []string{}
  179. for _, mline := range mlines {
  180. slice = append(slice, mline.data)
  181. }
  182. s := strings.Join(slice, "\n")
  183. // We need to tack on an extra newline at the end of the file
  184. return s + "\n"
  185. }
  186. func (mlines mungeLines) Bytes() []byte {
  187. return []byte(mlines.String())
  188. }
  189. var (
  190. // Finds all preformatted block start/stops.
  191. preformatRE = regexp.MustCompile("^\\s*```")
  192. notPreformatRE = regexp.MustCompile("^\\s*```.*```")
  193. // Is this line a header?
  194. mlHeaderRE = regexp.MustCompile(`^#`)
  195. // Is there a link on this line?
  196. mlLinkRE = regexp.MustCompile(`\[[^]]*\]\([^)]*\)`)
  197. beginTagRE = regexp.MustCompile(`<!-- BEGIN MUNGE:`)
  198. endTagRE = regexp.MustCompile(`<!-- END MUNGE:`)
  199. blankMungeLine = newMungeLine("")
  200. )
  201. // Does not set 'preformatted'
  202. func newMungeLine(line string) mungeLine {
  203. return mungeLine{
  204. data: line,
  205. header: mlHeaderRE.MatchString(line),
  206. link: mlLinkRE.MatchString(line),
  207. beginTag: beginTagRE.MatchString(line),
  208. endTag: endTagRE.MatchString(line),
  209. }
  210. }
  211. func trimRightSpace(in string) string {
  212. return strings.TrimRightFunc(in, unicode.IsSpace)
  213. }
  214. // Splits a document up into a slice of lines.
  215. func splitLines(document string) []string {
  216. lines := strings.Split(document, "\n")
  217. // Skip trailing empty string from Split-ing
  218. if len(lines) > 0 && lines[len(lines)-1] == "" {
  219. lines = lines[:len(lines)-1]
  220. }
  221. return lines
  222. }
  223. func getMungeLines(in string) mungeLines {
  224. var out mungeLines
  225. preformatted := false
  226. lines := splitLines(in)
  227. // We indicate if any given line is inside a preformatted block or
  228. // outside a preformatted block
  229. for _, line := range lines {
  230. if !preformatted {
  231. if preformatRE.MatchString(line) && !notPreformatRE.MatchString(line) {
  232. preformatted = true
  233. }
  234. } else {
  235. if preformatRE.MatchString(line) {
  236. preformatted = false
  237. }
  238. }
  239. ml := newMungeLine(line)
  240. ml.preformatted = preformatted
  241. out = append(out, ml)
  242. }
  243. return out
  244. }
  245. // filePath is the file we are looking for
  246. // inFile is the file where we found the link. So if we are processing
  247. // /path/to/repoRoot/docs/admin/README.md and are looking for
  248. // ../../file.json we can find that location.
  249. // In many cases filePath and processingFile may be the same
  250. func makeRepoRelative(filePath string, processingFile string) (string, error) {
  251. if filePath, err := filepath.Rel(repoRoot, filePath); err == nil {
  252. return filePath, nil
  253. }
  254. cwd := path.Dir(processingFile)
  255. return filepath.Rel(repoRoot, path.Join(cwd, filePath))
  256. }
  257. func makeFileRelative(filePath string, processingFile string) (string, error) {
  258. cwd := path.Dir(processingFile)
  259. if filePath, err := filepath.Rel(cwd, filePath); err == nil {
  260. return filePath, nil
  261. }
  262. return filepath.Rel(cwd, path.Join(cwd, filePath))
  263. }