| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- package dashdiffs
- import (
- "bytes"
- "errors"
- "fmt"
- "html/template"
- "sort"
- diff "github.com/yudai/gojsondiff"
- )
- type ChangeType int
- const (
- ChangeNil ChangeType = iota
- ChangeAdded
- ChangeDeleted
- ChangeOld
- ChangeNew
- ChangeUnchanged
- )
- var (
- // changeTypeToSymbol is used for populating the terminating characer in
- // the diff
- changeTypeToSymbol = map[ChangeType]string{
- ChangeNil: "",
- ChangeAdded: "+",
- ChangeDeleted: "-",
- ChangeOld: "-",
- ChangeNew: "+",
- }
- // changeTypeToName is used for populating class names in the diff
- changeTypeToName = map[ChangeType]string{
- ChangeNil: "same",
- ChangeAdded: "added",
- ChangeDeleted: "deleted",
- ChangeOld: "old",
- ChangeNew: "new",
- }
- )
- var (
- // tplJSONDiffWrapper is the template that wraps a diff
- tplJSONDiffWrapper = `{{ define "JSONDiffWrapper" -}}
- {{ range $index, $element := . }}
- {{ template "JSONDiffLine" $element }}
- {{ end }}
- {{ end }}`
- // tplJSONDiffLine is the template that prints each line in a diff
- tplJSONDiffLine = `{{ define "JSONDiffLine" -}}
- <p id="l{{ .LineNum }}" class="diff-line diff-json-{{ cton .Change }}">
- <span class="diff-line-number">
- {{if .LeftLine }}{{ .LeftLine }}{{ end }}
- </span>
- <span class="diff-line-number">
- {{if .RightLine }}{{ .RightLine }}{{ end }}
- </span>
- <span class="diff-value diff-indent-{{ .Indent }}" title="{{ .Text }}">
- {{ .Text }}
- </span>
- <span class="diff-line-icon">{{ ctos .Change }}</span>
- </p>
- {{ end }}`
- )
- var diffTplFuncs = template.FuncMap{
- "ctos": func(c ChangeType) string {
- if symbol, ok := changeTypeToSymbol[c]; ok {
- return symbol
- }
- return ""
- },
- "cton": func(c ChangeType) string {
- if name, ok := changeTypeToName[c]; ok {
- return name
- }
- return ""
- },
- }
- // JSONLine contains the data required to render each line of the JSON diff
- // and contains the data required to produce the tokens output in the basic
- // diff.
- type JSONLine struct {
- LineNum int `json:"line"`
- LeftLine int `json:"leftLine"`
- RightLine int `json:"rightLine"`
- Indent int `json:"indent"`
- Text string `json:"text"`
- Change ChangeType `json:"changeType"`
- Key string `json:"key"`
- Val interface{} `json:"value"`
- }
- func NewJSONFormatter(left interface{}) *JSONFormatter {
- tpl := template.Must(template.New("JSONDiffWrapper").Funcs(diffTplFuncs).Parse(tplJSONDiffWrapper))
- tpl = template.Must(tpl.New("JSONDiffLine").Funcs(diffTplFuncs).Parse(tplJSONDiffLine))
- return &JSONFormatter{
- left: left,
- Lines: []*JSONLine{},
- tpl: tpl,
- path: []string{},
- size: []int{},
- lineCount: 0,
- inArray: []bool{},
- }
- }
- type JSONFormatter struct {
- left interface{}
- path []string
- size []int
- inArray []bool
- lineCount int
- leftLine int
- rightLine int
- line *AsciiLine
- Lines []*JSONLine
- tpl *template.Template
- }
- type AsciiLine struct {
- // the type of change
- change ChangeType
- // the actual changes - no formatting
- key string
- val interface{}
- // level of indentation for the current line
- indent int
- // buffer containing the fully formatted line
- buffer *bytes.Buffer
- }
- func (f *JSONFormatter) Format(diff diff.Diff) (result string, err error) {
- if v, ok := f.left.(map[string]interface{}); ok {
- f.formatObject(v, diff)
- } else if v, ok := f.left.([]interface{}); ok {
- f.formatArray(v, diff)
- } else {
- return "", fmt.Errorf("expected map[string]interface{} or []interface{}, got %T",
- f.left)
- }
- b := &bytes.Buffer{}
- err = f.tpl.ExecuteTemplate(b, "JSONDiffWrapper", f.Lines)
- if err != nil {
- fmt.Printf("%v\n", err)
- return "", err
- }
- return b.String(), nil
- }
- func (f *JSONFormatter) formatObject(left map[string]interface{}, df diff.Diff) {
- f.addLineWith(ChangeNil, "{")
- f.push("ROOT", len(left), false)
- f.processObject(left, df.Deltas())
- f.pop()
- f.addLineWith(ChangeNil, "}")
- }
- func (f *JSONFormatter) formatArray(left []interface{}, df diff.Diff) {
- f.addLineWith(ChangeNil, "[")
- f.push("ROOT", len(left), true)
- f.processArray(left, df.Deltas())
- f.pop()
- f.addLineWith(ChangeNil, "]")
- }
- func (f *JSONFormatter) processArray(array []interface{}, deltas []diff.Delta) error {
- patchedIndex := 0
- for index, value := range array {
- f.processItem(value, deltas, diff.Index(index))
- patchedIndex++
- }
- // additional Added
- for _, delta := range deltas {
- switch delta.(type) {
- case *diff.Added:
- d := delta.(*diff.Added)
- // skip items already processed
- if int(d.Position.(diff.Index)) < len(array) {
- continue
- }
- f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
- }
- }
- return nil
- }
- func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []diff.Delta) error {
- names := sortKeys(object)
- for _, name := range names {
- value := object[name]
- f.processItem(value, deltas, diff.Name(name))
- }
- // Added
- for _, delta := range deltas {
- switch delta.(type) {
- case *diff.Added:
- d := delta.(*diff.Added)
- f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
- }
- }
- return nil
- }
- func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, position diff.Position) error {
- matchedDeltas := f.searchDeltas(deltas, position)
- positionStr := position.String()
- if len(matchedDeltas) > 0 {
- for _, matchedDelta := range matchedDeltas {
- switch matchedDelta.(type) {
- case *diff.Object:
- d := matchedDelta.(*diff.Object)
- switch value.(type) {
- case map[string]interface{}:
- //ok
- default:
- return errors.New("Type mismatch")
- }
- o := value.(map[string]interface{})
- f.newLine(ChangeNil)
- f.printKey(positionStr)
- f.print("{")
- f.closeLine()
- f.push(positionStr, len(o), false)
- f.processObject(o, d.Deltas)
- f.pop()
- f.newLine(ChangeNil)
- f.print("}")
- f.printComma()
- f.closeLine()
- case *diff.Array:
- d := matchedDelta.(*diff.Array)
- switch value.(type) {
- case []interface{}:
- //ok
- default:
- return errors.New("Type mismatch")
- }
- a := value.([]interface{})
- f.newLine(ChangeNil)
- f.printKey(positionStr)
- f.print("[")
- f.closeLine()
- f.push(positionStr, len(a), true)
- f.processArray(a, d.Deltas)
- f.pop()
- f.newLine(ChangeNil)
- f.print("]")
- f.printComma()
- f.closeLine()
- case *diff.Added:
- d := matchedDelta.(*diff.Added)
- f.printRecursive(positionStr, d.Value, ChangeAdded)
- f.size[len(f.size)-1]++
- case *diff.Modified:
- d := matchedDelta.(*diff.Modified)
- savedSize := f.size[len(f.size)-1]
- f.printRecursive(positionStr, d.OldValue, ChangeOld)
- f.size[len(f.size)-1] = savedSize
- f.printRecursive(positionStr, d.NewValue, ChangeNew)
- case *diff.TextDiff:
- savedSize := f.size[len(f.size)-1]
- d := matchedDelta.(*diff.TextDiff)
- f.printRecursive(positionStr, d.OldValue, ChangeOld)
- f.size[len(f.size)-1] = savedSize
- f.printRecursive(positionStr, d.NewValue, ChangeNew)
- case *diff.Deleted:
- d := matchedDelta.(*diff.Deleted)
- f.printRecursive(positionStr, d.Value, ChangeDeleted)
- default:
- return errors.New("Unknown Delta type detected")
- }
- }
- } else {
- f.printRecursive(positionStr, value, ChangeUnchanged)
- }
- return nil
- }
- func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
- results = make([]diff.Delta, 0)
- for _, delta := range deltas {
- switch delta.(type) {
- case diff.PostDelta:
- if delta.(diff.PostDelta).PostPosition() == position {
- results = append(results, delta)
- }
- case diff.PreDelta:
- if delta.(diff.PreDelta).PrePosition() == position {
- results = append(results, delta)
- }
- default:
- panic("heh")
- }
- }
- return
- }
- func (f *JSONFormatter) push(name string, size int, array bool) {
- f.path = append(f.path, name)
- f.size = append(f.size, size)
- f.inArray = append(f.inArray, array)
- }
- func (f *JSONFormatter) pop() {
- f.path = f.path[0 : len(f.path)-1]
- f.size = f.size[0 : len(f.size)-1]
- f.inArray = f.inArray[0 : len(f.inArray)-1]
- }
- func (f *JSONFormatter) addLineWith(change ChangeType, value string) {
- f.line = &AsciiLine{
- change: change,
- indent: len(f.path),
- buffer: bytes.NewBufferString(value),
- }
- f.closeLine()
- }
- func (f *JSONFormatter) newLine(change ChangeType) {
- f.line = &AsciiLine{
- change: change,
- indent: len(f.path),
- buffer: bytes.NewBuffer([]byte{}),
- }
- }
- func (f *JSONFormatter) closeLine() {
- leftLine := 0
- rightLine := 0
- f.lineCount++
- switch f.line.change {
- case ChangeAdded, ChangeNew:
- f.rightLine++
- rightLine = f.rightLine
- case ChangeDeleted, ChangeOld:
- f.leftLine++
- leftLine = f.leftLine
- case ChangeNil, ChangeUnchanged:
- f.rightLine++
- f.leftLine++
- rightLine = f.rightLine
- leftLine = f.leftLine
- }
- s := f.line.buffer.String()
- f.Lines = append(f.Lines, &JSONLine{
- LineNum: f.lineCount,
- RightLine: rightLine,
- LeftLine: leftLine,
- Indent: f.line.indent,
- Text: s,
- Change: f.line.change,
- Key: f.line.key,
- Val: f.line.val,
- })
- }
- func (f *JSONFormatter) printKey(name string) {
- if !f.inArray[len(f.inArray)-1] {
- f.line.key = name
- fmt.Fprintf(f.line.buffer, `"%s": `, name)
- }
- }
- func (f *JSONFormatter) printComma() {
- f.size[len(f.size)-1]--
- if f.size[len(f.size)-1] > 0 {
- f.line.buffer.WriteRune(',')
- }
- }
- func (f *JSONFormatter) printValue(value interface{}) {
- switch value.(type) {
- case string:
- f.line.val = value
- fmt.Fprintf(f.line.buffer, `"%s"`, value)
- case nil:
- f.line.val = "null"
- f.line.buffer.WriteString("null")
- default:
- f.line.val = value
- fmt.Fprintf(f.line.buffer, `%#v`, value)
- }
- }
- func (f *JSONFormatter) print(a string) {
- f.line.buffer.WriteString(a)
- }
- func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
- switch value.(type) {
- case map[string]interface{}:
- f.newLine(change)
- f.printKey(name)
- f.print("{")
- f.closeLine()
- m := value.(map[string]interface{})
- size := len(m)
- f.push(name, size, false)
- keys := sortKeys(m)
- for _, key := range keys {
- f.printRecursive(key, m[key], change)
- }
- f.pop()
- f.newLine(change)
- f.print("}")
- f.printComma()
- f.closeLine()
- case []interface{}:
- f.newLine(change)
- f.printKey(name)
- f.print("[")
- f.closeLine()
- s := value.([]interface{})
- size := len(s)
- f.push("", size, true)
- for _, item := range s {
- f.printRecursive("", item, change)
- }
- f.pop()
- f.newLine(change)
- f.print("]")
- f.printComma()
- f.closeLine()
- default:
- f.newLine(change)
- f.printKey(name)
- f.printValue(value)
- f.printComma()
- f.closeLine()
- }
- }
- func sortKeys(m map[string]interface{}) (keys []string) {
- keys = make([]string, 0, len(m))
- for key := range m {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- return
- }
|