/**
* @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
}