all files / util/ diff.js

6.06% Statements 4/66
0% Branches 0/44
0% Functions 0/4
6.06% Lines 4/66
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112                                                                                                                                                                                                                       
import isString from './isString'
import levenshtein from './levenshtein'
 
/*
  Determines a list of changes to transform String a into String b.
 
  @param {String} a
  @param {String} b
*/
function diff(a, b, offset) {
  if (!isString(a) || !isString(b)) {
    throw new Error('Illegal arguments.')
  }
  offset = offset || 0
  let changes = []
  if (a || b) {
    if (!a && b) {
      changes.push({ type:'insert', start:offset, text:b })
    } else if (a && !b) {
      changes.push({ type:'delete', start:offset, end:offset+a.length })
    } else {
      let m = levenshtein(a, b)
      changes = _diff(a, b, m, offset)
    }
  }
  return changes
}
 
function _diff(a, b, m, offset) {
  let i = b.length
  let j = a.length
  let changes = []
  let current
  while (i>0 && j>0) {
    _next()
  }
  _commit()
  return changes
 
  function _next() {
    let d = m[i][j]
    let ib = i-1
    let jb = j-1
    // substitute
    if (m[ib][jb]<d) {
      if (current && current.type === 'replace') {
        current.start--
        current.text.unshift(b.charAt(ib))
      } else {
        _commit()
        current = { type:'replace', start:jb, end:j, text:[b.charAt(ib)] }
      }
      i--
      j--
    }
    // insert
    else if (m[ib][j]<d) {
      if (current && current.type === 'insert') {
        current.start--
        current.text.unshift(b.charAt(ib))
      } else {
        _commit()
        current = { type:'insert', start:jb, text:[b.charAt(ib)] }
      }
      i--
    }
    // delete char
    else if (m[i][jb]<d) {
      if (current && current.type === 'delete') {
        current.start--
      } else {
        _commit()
        current = { type:'delete', start:jb, end:j }
      }
      j--
    }
    // preserve
    else {
      _commit()
      i--
      j--
    }
  }
 
  function _commit() {
    if (current) {
      switch (current.type) {
        case 'insert':
          current.start += offset
          current.text = current.text.join('')
          break
        case 'delete':
          current.start += offset
          current.end += offset
          break
        case 'replace':
          current.start += offset
          current.end += offset
          current.text = current.text.join('')
          break
        default:
          throw new Error('Invalid state')
      }
      changes.push(current)
      current = null
    }
  }
 
}
 
export default diff