text: /**
   * @namespace formatter
   * @memberof module:kdljs
   */
  
  const { validateDocument } = require('./validator.js')
  
  /* eslint-disable no-control-regex */
  const linespace = /^[\r\n\u0085\u000C\u2028\u2029]$/
  const nonAscii = /^[^\x00-\x7F]$/
  const nonPrintableAscii = /^[\x00-\x19\x7F]$/
  /* eslint-enable no-control-regex */
  const commonEscapes = {
    '\n': '\\n',
    '\r': '\\r',
    '\t': '\\t',
    '\\': '\\\\',
    '"': '\\"',
    '/': '\\/',
    '\x08': '\\b',
    '\x0C': '\\f'
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {string} char
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @return {boolean}
   */
  function shouldEscapeChar (value, options) {
    const charCode = value.charCodeAt(0)
  
    if (options.escapes[charCode]) {
      return true
    } else if (value === '"' || value === '\\') {
      return true
    } else if (options.escapeCommon && value in commonEscapes) {
      return true
    } else if (options.escapeNonPrintableAscii && nonPrintableAscii.test(value)) {
      return true
    } else if (options.escapeLinespace && linespace.test(value)) {
      return true
    } else if (options.escapeNonAscii && nonAscii.test(value)) {
      return true
    } else {
      return false
    }
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {string} char
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @return {string}
   */
  function formatChar (value, options) {
    if (!shouldEscapeChar(value, options)) {
      return value
    } else if (value in commonEscapes) {
      return commonEscapes[value]
    } else {
      return `\\u{${value.charCodeAt(0).toString(16)}}`
    }
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {string} value
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @return {string}
   */
  function formatString (value, options) {
    return `"${[...value].map(char => formatChar(char, options)).join('')}"`
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {string} value
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @return {string}
   */
  function formatIdentifier (value, options) {
    if (/^(?![+-]\d)[\x21\x23-\x27\x2A\x2B\x2D\x2E\x3A\x3F-\x5A\x5E-\x7A\x7C\x7E-\uFFFF][\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x3A\x3F-\x5A\x5E-\x7A\x7C\x7E-\uFFFF]*$/.test(value)) {
      return value
    } else {
      return formatString(value, options)
    }
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {string} [value] - KDL tag
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @return {string}
   */
  function formatTag (value, options) {
    return value === undefined ? '' : '(' + formatIdentifier(value, options) + ')'
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {module:kdljs~Value} value - KDL value
   * @param {string} [tag] - KDL tag
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @return {string}
   */
  function formatValue (value, tag, options) {
    if (typeof value === 'string') {
      return formatTag(tag, options) + formatString(value, options)
    } else if (typeof value === 'number' && options.exponentChar === 'E') {
      return formatTag(tag, options) + value.toString().toUpperCase()
    } else {
      return formatTag(tag, options) + value
    }
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {string} key
   * @param {module:kdljs~Value} value - KDL value
   * @param {string} [tag] - KDL tag
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @return {string}
   */
  function formatProperty (key, value, tag, options) {
    return formatIdentifier(key, options) + '=' + formatValue(value, tag, options)
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {module:kdljs~Node} node - KDL node
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @param {number} indent
   * @return {string}
   */
  function formatNode (node, options, indent) {
    let values = node.values
    if (!options.printNullArgs) {
      values = values.filter(value => value !== null)
    }
  
    let properties = Object.keys(node.properties).sort()
    if (!options.printNullProps) {
      properties = properties.filter(key => node.properties[key] !== null)
    }
  
    const currentIndent = options.indentChar.repeat(options.indent * indent)
    const parts = [
      currentIndent + formatTag(node.tags.name, options) + formatIdentifier(node.name, options),
      ...values.map((value, index) => formatValue(value, node.tags.values[index], options)),
      ...properties.map(key => formatProperty(key, node.properties[key], node.tags.properties[key], options))
    ]
  
    if (node.children.length) {
      indent++
      parts.push('{' + options.newline + formatDocument(node.children, options, indent) + options.newline + currentIndent + '}')
      indent--
    } else if (options.printEmptyChildren) {
      parts.push('{}')
    }
  
    return options.requireSemicolons ? parts.join(' ') + ';' : parts.join(' ')
  }
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {module:kdljs~Document} doc - KDL document
   * @param {module:kdljs/formatter.ProcessedOptions} options - Formatting options
   * @param {number} indent
   * @return {string}
   */
  function formatDocument (doc, options, indent) {
    const nodes = doc.map(node => formatNode(node, options, indent))
  
    return nodes.join(options.newline)
  }
  
  /**
   * @access private
   * @typedef ProcessedOptions
   * @memberof module:kdljs.formatter
   * @type {Object}
   * @property {Object<number,boolean>} escapes
   * @property {boolean} requireSemicolons
   * @property {boolean} escapeNonAscii
   * @property {boolean} escapeNonPrintableAscii
   * @property {boolean} escapeCommon
   * @property {boolean} escapeLinespace
   * @property {string} newline
   * @property {number} indent
   * @property {string} indentChar
   * @property {string} exponentChar
   * @property {boolean} printEmptyChildren
   * @property {boolean} printNullArgs
   * @property {boolean} printNullProps
   */
  
  /**
   * @access private
   * @memberof module:kdljs.formatter
   * @param {module:kdljs/formatter.Options} options - Formatting options
   * @return {module:kdljs/formatter.ProcessedOptions}
   */
  function processOptions (options) {
    return {
      escapes: {},
      requireSemicolons: false,
      escapeNonAscii: false,
      escapeNonPrintableAscii: true,
      escapeCommon: true,
      escapeLinespace: true,
      newline: '\n',
      indent: 4,
      indentChar: ' ',
      exponentChar: 'E',
      printEmptyChildren: false,
      printNullArgs: true,
      printNullProps: true,
      ...options
    }
  }
  
  /**
   * @typedef Options
   * @memberof module:kdljs.formatter
   * @type {Object}
   * @property {Object<number,boolean>} [escapes={}] - A map of which characters to escape (by decimal codepoint)
   * @property {boolean} [requireSemicolons=false] - Whether to print semicolons after each node
   * @property {boolean} [escapeNonAscii=false] - Whether to escape any non-ASCII characters
   * @property {boolean} [escapeNonPrintableAscii=true] - Whether to escape non-printable ASCII characters
   * @property {boolean} [escapeCommon=true] - Whether to escape common characters (i.e. those with single-character escape codes)
   * @property {boolean} [escapeLinespace=true] - Whether to escape linespace characters
   * @property {string} [newline='\n'] - The newline character
   * @property {number} [indent=4] - The number of characters (from `indentChar`) to indent each level with
   * @property {string} [indentChar=' '] - What character to indent with
   * @property {string} [exponentChar='E'] - What character to use for the exponent in floats (`e` or `E`)
   * @property {boolean} [printEmptyChildren=false] - Whether to print empty brackets if a node has no children
   * @property {boolean} [printNullArgs=true] - Whether to print `null` values
   * @property {boolean} [printNullProps=true] - Whether to print properties with value `null`
   */
  
  /**
   * @function format
   * @memberof module:kdljs.formatter
   * @param {module:kdljs~Document} doc - Input KDL document
   * @param {module:kdljs.formatter.Options} [options={}] - Formatting options
   * @return {string} formatted KDL file
   */
  module.exports.format = function format (doc, options) {
    if (!validateDocument(doc)) {
      throw new TypeError('Invalid KDL document')
    }
  
    const processsedOptions = processOptions(options || {})
  
    return formatDocument(doc, processsedOptions, 0) + processsedOptions.newline
  }
  