swagger-api/swagger-editor

View on GitHub
src/plugins/ast/ast.js

Summary

Maintainability
F
3 days
Test Coverage
import YAML from "yaml-js"
import isArray from "lodash/isArray"
import lodashFind from "lodash/find"
import memoize from "lodash/memoize"

let cachedCompose = memoize(YAML.compose) // TODO: build a custom cache based on content

var MAP_TAG = "tag:yaml.org,2002:map"
var SEQ_TAG = "tag:yaml.org,2002:seq"

export function getLineNumberForPath(yaml, path) {

  // Type check
  if (typeof yaml !== "string") {
    throw new TypeError("yaml should be a string")
  }
  if (!isArray(path)) {
    throw new TypeError("path should be an array of strings")
  }

  var i = 0

  let ast = cachedCompose(yaml)

  // simply walks the tree using current path recursively to the point that
  // path is empty

  return find(ast, path)

  function find(current, path, last) {
    if(!current) {
      // something has gone quite wrong
      // return the last start_mark as a best-effort
      if(last && last.start_mark)
        return last.start_mark.line
      return 0
    }

    if (path.length && current.tag === MAP_TAG) {
      for (i = 0; i < current.value.length; i++) {
        var pair = current.value[i]
        var key = pair[0]
        var value = pair[1]

        if (key.value === path[0]) {
          return find(value, path.slice(1), current)
        }

        if (key.value === path[0].replace(/\[.*/, "")) {
          // access the array at the index in the path (example: grab the 2 in "tags[2]")
          var index = parseInt(path[0].match(/\[(.*)\]/)[1])
          if(value.value.length === 1 && index !== 0 && !!index) {
            var nextVal = lodashFind(value.value[0], { value: index.toString() })
          } else { // eslint-disable-next-line no-redeclare
            var nextVal = value.value[index]
          }
          return find(nextVal, path.slice(1), value.value)
        }
      }
    }

    if (path.length && current.tag === SEQ_TAG) {
      var item = current.value[path[0]]

      if (item && item.tag) {
        return find(item, path.slice(1), current.value)
      }
    }

    if (current.tag === MAP_TAG && !Array.isArray(last)) {
      return current.start_mark.line
    } else {
      return current.start_mark.line + 1
    }
  }
}

/**
* Get a position object with given
* @param  {string}   yaml
* YAML or JSON string
* @param  {array}   path
* an array of stings that constructs a
* JSON Path similar to JSON Pointers(RFC 6901). The difference is, each
* component of path is an item of the array instead of being separated with
* slash(/) in a string
*/
export function positionRangeForPath(yaml, path) {

  // Type check
  if (typeof yaml !== "string") {
    throw new TypeError("yaml should be a string")
  }
  if (!isArray(path)) {
    throw new TypeError("path should be an array of strings")
  }

  var invalidRange = {
    start: {line: -1, column: -1},
    end: {line: -1, column: -1}
  }
  var i = 0

  let ast = cachedCompose(yaml)

  // simply walks the tree using astValue path recursively to the point that
  // path is empty.
  return find(ast)

  function find(astValue, astKeyValue) {
    if (astValue.tag === MAP_TAG) {
      for (i = 0; i < astValue.value.length; i++) {
        var pair = astValue.value[i]
        var key = pair[0]
        var value = pair[1]

        if (key.value === path[0]) {
          path.shift()
          return find(value, key)
        }
      }
    }

    if (astValue.tag === SEQ_TAG) {
      var item = astValue.value[path[0]]

      if (item && item.tag) {
        path.shift()
        return find(item, astKeyValue)
      }
    }

    // if path is still not empty we were not able to find the node
    if (path.length) {
      return invalidRange
    }

    const range = {
      start: {
        line: astValue.start_mark.line,
        column: astValue.start_mark.column,
        pointer: astValue.start_mark.pointer,
      },
      end: {
        line: astValue.end_mark.line,
        column: astValue.end_mark.column,
        pointer: astValue.end_mark.pointer,
      }
    }

    if(astKeyValue) {
      // eslint-disable-next-line camelcase
      range.key_start = {
        line: astKeyValue.start_mark.line,
        column: astKeyValue.start_mark.column,
        pointer: astKeyValue.start_mark.pointer,
      }
      // eslint-disable-next-line camelcase
      range.key_end = {
        line: astKeyValue.end_mark.line,
        column: astKeyValue.end_mark.column,
        pointer: astKeyValue.end_mark.pointer,
      }
    }

    return range
  }
}

/**
* Get a JSON Path for position object in the spec
* @param  {string} yaml
* YAML or JSON string
* @param  {object} position
* position in the YAML or JSON string with `line` and `column` properties.
* `line` and `column` number are zero indexed
*/
export function pathForPosition(yaml, position) {

  // Type check
  if (typeof yaml !== "string") {
    throw new TypeError("yaml should be a string")
  }
  if (typeof position !== "object" || typeof position.line !== "number" ||
  typeof position.column !== "number") {
    throw new TypeError("position should be an object with line and column" +
    " properties")
  }

  try {
    var ast = cachedCompose(yaml)
  } catch (e) {
    console.error("Error composing AST", e)

    const problemMark = e.problem_mark || {}
    const errorTraceMessage = [
      yaml.split("\n").slice(problemMark.line - 5, problemMark.line + 1).join("\n"),
      Array(problemMark.column).fill(" ").join("") + `^----- ${e.name}: ${e.toString().split("\n")[0]}`,
      yaml.split("\n").slice(problemMark.line + 1, problemMark.line + 5).join("\n")
    ].join("\n")

    console.error(errorTraceMessage)
    return null
  }


  var path = []

  return find(ast)

  /**
  * recursive find function that finds the node matching the position
  * @param  {object} current - AST object to serach into
  */
  function find(current) {

    // algorythm:
    // is current a promitive?
    //   // finish recursion without modifying the path
    // is current a hash?
    //   // find a key or value that position is in their range
    //     // if key is in range, terminate recursion with exisiting path
    //     // if a value is in range push the corresponding key to the path
    //     //   andcontinue recursion
    // is current an array
    //   // find the item that position is in it"s range and push the index
    //   //  of the item to the path and continue recursion with that item.

    var i = 0

    if (!current || [MAP_TAG, SEQ_TAG].indexOf(current.tag) === -1) {
      return path
    }

    if (current.tag === MAP_TAG) {
      for (i = 0; i < current.value.length; i++) {
        var pair = current.value[i]
        var key = pair[0]
        var value = pair[1]

        if (isInRange(key)) {
          return path
        } else if (isInRange(value)) {
          path.push(key.value)
          return find(value)
        }
      }
    }

    if (current.tag === SEQ_TAG) {
      for (i = 0; i < current.value.length; i++) {
        var item = current.value[i]

        if (isInRange(item)) {
          path.push(i.toString())
          return find(item)
        }
      }
    }

    return path

    /**
    * Determines if position is in node"s range
    * @param  {object}  node - AST node
    * @return {Boolean}      true if position is in node"s range
    */
    function isInRange(node) {
      /* jshint camelcase: false */

      // if node is in a single line
      if (node.start_mark.line === node.end_mark.line) {

        return (position.line === node.start_mark.line) &&
        (node.start_mark.column <= position.column) &&
        (node.end_mark.column >= position.column)
      }

      // if position is in the same line as start_mark
      if (position.line === node.start_mark.line) {
        return position.column >= node.start_mark.column
      }

      // if position is in the same line as end_mark
      if (position.line === node.end_mark.line) {
        return position.column <= node.end_mark.column
      }

      // if position is between start and end lines return true, otherwise
      // return false.
      return (node.start_mark.line < position.line) &&
      (node.end_mark.line > position.line)
    }
  }
}

// utility fns


export let pathForPositionAsync = promisifySyncFn(pathForPosition)
export let positionRangeForPathAsync = promisifySyncFn(positionRangeForPath)
export let getLineNumberForPathAsync = promisifySyncFn(getLineNumberForPath)

function promisifySyncFn(fn) {
  return function(...args) {
    return new Promise((resolve) => resolve(fn(...args)))
  }
}