formatter_basic.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. package dashdiffs
  2. import (
  3. "bytes"
  4. "html/template"
  5. diff "github.com/yudai/gojsondiff"
  6. )
  7. // A BasicDiff holds the stateful values that are used when generating a basic
  8. // diff from JSON tokens.
  9. type BasicDiff struct {
  10. narrow string
  11. keysIdent int
  12. writing bool
  13. LastIndent int
  14. Block *BasicBlock
  15. Change *BasicChange
  16. Summary *BasicSummary
  17. }
  18. // A BasicBlock represents a top-level element in a basic diff.
  19. type BasicBlock struct {
  20. Title string
  21. Old interface{}
  22. New interface{}
  23. Change ChangeType
  24. Changes []*BasicChange
  25. Summaries []*BasicSummary
  26. LineStart int
  27. LineEnd int
  28. }
  29. // A BasicChange represents the change from an old to new value. There are many
  30. // BasicChanges in a BasicBlock.
  31. type BasicChange struct {
  32. Key string
  33. Old interface{}
  34. New interface{}
  35. Change ChangeType
  36. LineStart int
  37. LineEnd int
  38. }
  39. // A BasicSummary represents the changes within a basic block that're too deep
  40. // or verbose to be represented in the top-level BasicBlock element, or in the
  41. // BasicChange. Instead of showing the values in this case, we simply print
  42. // the key and count how many times the given change was applied to that
  43. // element.
  44. type BasicSummary struct {
  45. Key string
  46. Change ChangeType
  47. Count int
  48. LineStart int
  49. LineEnd int
  50. }
  51. type BasicFormatter struct {
  52. jsonDiff *JSONFormatter
  53. tpl *template.Template
  54. }
  55. func NewBasicFormatter(left interface{}) *BasicFormatter {
  56. tpl := template.Must(template.New("block").Funcs(tplFuncMap).Parse(tplBlock))
  57. tpl = template.Must(tpl.New("change").Funcs(tplFuncMap).Parse(tplChange))
  58. tpl = template.Must(tpl.New("summary").Funcs(tplFuncMap).Parse(tplSummary))
  59. return &BasicFormatter{
  60. jsonDiff: NewJSONFormatter(left),
  61. tpl: tpl,
  62. }
  63. }
  64. // Format takes the diff of two JSON documents, and returns the difference
  65. // between them summarized in an HTML document.
  66. func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
  67. // calling jsonDiff.Format(d) populates the JSON diff's "Lines" value,
  68. // which we use to compute the basic dif
  69. _, err := b.jsonDiff.Format(d)
  70. if err != nil {
  71. return nil, err
  72. }
  73. bd := &BasicDiff{}
  74. blocks := bd.Basic(b.jsonDiff.Lines)
  75. buf := &bytes.Buffer{}
  76. err = b.tpl.ExecuteTemplate(buf, "block", blocks)
  77. if err != nil {
  78. return nil, err
  79. }
  80. return buf.Bytes(), nil
  81. }
  82. // Basic transforms a slice of JSONLines into a slice of BasicBlocks.
  83. func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
  84. // init an array you can append to for the basic "blocks"
  85. blocks := make([]*BasicBlock, 0)
  86. for _, line := range lines {
  87. // In order to produce distinct "blocks" when rendering the basic diff,
  88. // we need a way to distinguish between differnt sections of data.
  89. // To do this, we consider the value(s) of each top-level JSON key to
  90. // represent a distinct block for Grafana's JSON data structure, so
  91. // we perform this check to see if we've entered a new "block". If we
  92. // have, we simply append the existing block to the array of blocks.
  93. if b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil {
  94. if b.Block != nil {
  95. blocks = append(blocks, b.Block)
  96. }
  97. }
  98. // Record the last indent level at each pass in case we need to
  99. // check for a change in depth inside the JSON data structures.
  100. b.LastIndent = line.Indent
  101. // TODO: why special handling for indent 2?
  102. // Here we
  103. // If the line's indentation is at level 1, then we know it's a top
  104. // level key in the JSON document. As mentioned earlier, we treat these
  105. // specially as they indicate their values belong to distinct blocks.
  106. //
  107. // At level 1, we only record single-line changes, ie, the "added",
  108. // "deleted", "old" or "new" cases, since we know those values aren't
  109. // arrays or maps. We only handle these cases at level 2 or deeper,
  110. // since for those we either output a "change" or "summary". This is
  111. // done for formatting reasons only, so we have logical "blocks" to
  112. // display.
  113. if line.Indent == 1 {
  114. switch line.Change {
  115. case ChangeNil:
  116. if line.Change == ChangeNil {
  117. if line.Key != "" {
  118. b.Block = &BasicBlock{
  119. Title: line.Key,
  120. Change: line.Change,
  121. }
  122. }
  123. }
  124. case ChangeAdded, ChangeDeleted:
  125. blocks = append(blocks, &BasicBlock{
  126. Title: line.Key,
  127. Change: line.Change,
  128. New: line.Val,
  129. LineStart: line.LineNum,
  130. })
  131. case ChangeOld:
  132. b.Block = &BasicBlock{
  133. Title: line.Key,
  134. Old: line.Val,
  135. Change: line.Change,
  136. LineStart: line.LineNum,
  137. }
  138. case ChangeNew:
  139. b.Block.New = line.Val
  140. b.Block.LineEnd = line.LineNum
  141. // For every "old" change there is a corresponding "new", which
  142. // is why we wait until we detect the "new" change before
  143. // appending the change.
  144. blocks = append(blocks, b.Block)
  145. default:
  146. // ok
  147. }
  148. }
  149. // Here is where we handle changes for all types, appending each change
  150. // to the current block based on the value.
  151. //
  152. // Values which only occupy a single line in JSON (like a string or
  153. // int, for example) are treated as "Basic Changes" that we append to
  154. // the current block as soon as they're detected.
  155. //
  156. // Values which occupy multiple lines (either slices or maps) are
  157. // treated as "Basic Summaries". When we detect the "ChangeNil" type,
  158. // we know we've encountered one of these types, so we record the
  159. // starting position as well the type of the change, and stop
  160. // performing comparisons until we find the end of that change. Upon
  161. // finding the change, we append it to the current block, and begin
  162. // performing comparisons again.
  163. if line.Indent > 1 {
  164. // Ensure a single line change
  165. if line.Key != "" && line.Val != nil && !b.writing {
  166. switch line.Change {
  167. case ChangeAdded, ChangeDeleted:
  168. b.Block.Changes = append(b.Block.Changes, &BasicChange{
  169. Key: line.Key,
  170. Change: line.Change,
  171. New: line.Val,
  172. LineStart: line.LineNum,
  173. })
  174. case ChangeOld:
  175. b.Change = &BasicChange{
  176. Key: line.Key,
  177. Change: line.Change,
  178. Old: line.Val,
  179. LineStart: line.LineNum,
  180. }
  181. case ChangeNew:
  182. b.Change.New = line.Val
  183. b.Change.LineEnd = line.LineNum
  184. b.Block.Changes = append(b.Block.Changes, b.Change)
  185. default:
  186. //ok
  187. }
  188. } else {
  189. if line.Change != ChangeUnchanged {
  190. if line.Key != "" {
  191. b.narrow = line.Key
  192. b.keysIdent = line.Indent
  193. }
  194. if line.Change != ChangeNil {
  195. if !b.writing {
  196. b.writing = true
  197. key := b.Block.Title
  198. if b.narrow != "" {
  199. key = b.narrow
  200. if b.keysIdent > line.Indent {
  201. key = b.Block.Title
  202. }
  203. }
  204. b.Summary = &BasicSummary{
  205. Key: key,
  206. Change: line.Change,
  207. LineStart: line.LineNum,
  208. }
  209. }
  210. }
  211. } else {
  212. if b.writing {
  213. b.writing = false
  214. b.Summary.LineEnd = line.LineNum
  215. b.Block.Summaries = append(b.Block.Summaries, b.Summary)
  216. }
  217. }
  218. }
  219. }
  220. }
  221. return blocks
  222. }
  223. // encStateMap is used in the template helper
  224. var (
  225. encStateMap = map[ChangeType]string{
  226. ChangeAdded: "added",
  227. ChangeDeleted: "deleted",
  228. ChangeOld: "changed",
  229. ChangeNew: "changed",
  230. }
  231. // tplFuncMap is the function map for each template
  232. tplFuncMap = template.FuncMap{
  233. "getChange": func(c ChangeType) string {
  234. state, ok := encStateMap[c]
  235. if !ok {
  236. return "changed"
  237. }
  238. return state
  239. },
  240. }
  241. )
  242. var (
  243. // tplBlock is the whole thing
  244. tplBlock = `{{ define "block" -}}
  245. {{ range . }}
  246. <div class="diff-group">
  247. <div class="diff-block">
  248. <h2 class="diff-block-title">
  249. <i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle"></i>
  250. <strong class="diff-title">{{ .Title }}</strong> {{ getChange .Change }}
  251. </h2>
  252. <!-- Overview -->
  253. {{ if .Old }}
  254. <div class="diff-label">{{ .Old }}</div>
  255. <i class="diff-arrow fa fa-long-arrow-right"></i>
  256. {{ end }}
  257. {{ if .New }}
  258. <div class="diff-label">{{ .New }}</div>
  259. {{ end }}
  260. {{ if .LineStart }}
  261. <diff-link-json
  262. line-link="{{ .LineStart }}"
  263. line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
  264. switch-view="ctrl.getDiff('html')"
  265. />
  266. {{ end }}
  267. </div>
  268. <!-- Basic Changes -->
  269. {{ range .Changes }}
  270. <ul class="diff-change-container">
  271. {{ template "change" . }}
  272. </ul>
  273. {{ end }}
  274. <!-- Basic Summary -->
  275. {{ range .Summaries }}
  276. {{ template "summary" . }}
  277. {{ end }}
  278. </div>
  279. {{ end }}
  280. {{ end }}`
  281. // tplChange is the template for changes
  282. tplChange = `{{ define "change" -}}
  283. <li class="diff-change-group">
  284. <span class="bullet-position-container">
  285. <div class="diff-change-item diff-change-title">{{ getChange .Change }} {{ .Key }}</div>
  286. <div class="diff-change-item">
  287. {{ if .Old }}
  288. <div class="diff-label">{{ .Old }}</div>
  289. <i class="diff-arrow fa fa-long-arrow-right"></i>
  290. {{ end }}
  291. {{ if .New }}
  292. <div class="diff-label">{{ .New }}</div>
  293. {{ end }}
  294. </div>
  295. {{ if .LineStart }}
  296. <diff-link-json
  297. line-link="{{ .LineStart }}"
  298. line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
  299. switch-view="ctrl.getDiff('json')"
  300. />
  301. {{ end }}
  302. </span>
  303. </li>
  304. {{ end }}`
  305. // tplSummary is for basis summaries
  306. tplSummary = `{{ define "summary" -}}
  307. <div class="diff-group-name">
  308. <i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle-o diff-list-circle"></i>
  309. {{ if .Count }}
  310. <strong>{{ .Count }}</strong>
  311. {{ end }}
  312. {{ if .Key }}
  313. <strong class="diff-summary-key">{{ .Key }}</strong>
  314. {{ getChange .Change }}
  315. {{ end }}
  316. {{ if .LineStart }}
  317. <diff-link-json
  318. line-link="{{ .LineStart }}"
  319. line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
  320. switch-view="ctrl.getDiff('json')"
  321. />
  322. {{ end }}
  323. </div>
  324. {{ end }}`
  325. )