'use strict' var align = require('wide-align') var validate = require('aproba') var objectAssign = require('object-assign') var wideTruncate = require('./wide-truncate') var error = require('./error') var TemplateItem = require('./template-item') function renderValueWithValues (values) { return function (item) { return renderValue(item, values) } } var renderTemplate = module.exports = function (width, template, values) { var items = prepareItems(width, template, values) var rendered = items.map(renderValueWithValues(values)).join('') return align.left(wideTruncate(rendered, width), width) } function preType (item) { var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1) return 'pre' + cappedTypeName } function postType (item) { var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1) return 'post' + cappedTypeName } function hasPreOrPost (item, values) { if (!item.type) return return values[preType(item)] || values[postType(item)] } function generatePreAndPost (baseItem, parentValues) { var item = objectAssign({}, baseItem) var values = Object.create(parentValues) var template = [] var pre = preType(item) var post = postType(item) if (values[pre]) { template.push({value: values[pre]}) values[pre] = null } item.minLength = null item.length = null item.maxLength = null template.push(item) values[item.type] = values[item.type] if (values[post]) { template.push({value: values[post]}) values[post] = null } return function ($1, $2, length) { return renderTemplate(length, template, values) } } function prepareItems (width, template, values) { function cloneAndObjectify (item, index, arr) { var cloned = new TemplateItem(item, width) var type = cloned.type if (cloned.value == null) { if (!(type in values)) { if (cloned.default == null) { throw new error.MissingTemplateValue(cloned, values) } else { cloned.value = cloned.default } } else { cloned.value = values[type] } } if (cloned.value == null || cloned.value === '') return null cloned.index = index cloned.first = index === 0 cloned.last = index === arr.length - 1 if (hasPreOrPost(cloned, values)) cloned.value = generatePreAndPost(cloned, values) return cloned } var output = template.map(cloneAndObjectify).filter(function (item) { return item != null }) var outputLength = 0 var remainingSpace = width var variableCount = output.length function consumeSpace (length) { if (length > remainingSpace) length = remainingSpace outputLength += length remainingSpace -= length } function finishSizing (item, length) { if (item.finished) throw new error.Internal('Tried to finish template item that was already finished') if (length === Infinity) throw new error.Internal('Length of template item cannot be infinity') if (length != null) item.length = length item.minLength = null item.maxLength = null --variableCount item.finished = true if (item.length == null) item.length = item.getBaseLength() if (item.length == null) throw new error.Internal('Finished template items must have a length') consumeSpace(item.getLength()) } output.forEach(function (item) { if (!item.kerning) return var prevPadRight = item.first ? 0 : output[item.index - 1].padRight if (!item.first && prevPadRight < item.kerning) item.padLeft = item.kerning - prevPadRight if (!item.last) item.padRight = item.kerning }) // Finish any that have a fixed (literal or intuited) length output.forEach(function (item) { if (item.getBaseLength() == null) return finishSizing(item) }) var resized = 0 var resizing var hunkSize do { resizing = false hunkSize = Math.round(remainingSpace / variableCount) output.forEach(function (item) { if (item.finished) return if (!item.maxLength) return if (item.getMaxLength() < hunkSize) { finishSizing(item, item.maxLength) resizing = true } }) } while (resizing && resized++ < output.length) if (resizing) throw new error.Internal('Resize loop iterated too many times while determining maxLength') resized = 0 do { resizing = false hunkSize = Math.round(remainingSpace / variableCount) output.forEach(function (item) { if (item.finished) return if (!item.minLength) return if (item.getMinLength() >= hunkSize) { finishSizing(item, item.minLength) resizing = true } }) } while (resizing && resized++ < output.length) if (resizing) throw new error.Internal('Resize loop iterated too many times while determining minLength') hunkSize = Math.round(remainingSpace / variableCount) output.forEach(function (item) { if (item.finished) return finishSizing(item, hunkSize) }) return output } function renderFunction (item, values, length) { validate('OON', arguments) if (item.type) { return item.value(values, values[item.type + 'Theme'] || {}, length) } else { return item.value(values, {}, length) } } function renderValue (item, values) { var length = item.getBaseLength() var value = typeof item.value === 'function' ? renderFunction(item, values, length) : item.value if (value == null || value === '') return '' var alignWith = align[item.align] || align.left var leftPadding = item.padLeft ? align.left('', item.padLeft) : '' var rightPadding = item.padRight ? align.right('', item.padRight) : '' var truncated = wideTruncate(String(value), length) var aligned = alignWith(truncated, length) return leftPadding + aligned + rightPadding }