| '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 |
| } |