1'use strict' 2var align = require('wide-align') 3var validate = require('aproba') 4var objectAssign = require('object-assign') 5var wideTruncate = require('./wide-truncate') 6var error = require('./error') 7var TemplateItem = require('./template-item') 8 9function renderValueWithValues (values) { 10 return function (item) { 11 return renderValue(item, values) 12 } 13} 14 15var renderTemplate = module.exports = function (width, template, values) { 16 var items = prepareItems(width, template, values) 17 var rendered = items.map(renderValueWithValues(values)).join('') 18 return align.left(wideTruncate(rendered, width), width) 19} 20 21function preType (item) { 22 var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1) 23 return 'pre' + cappedTypeName 24} 25 26function postType (item) { 27 var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1) 28 return 'post' + cappedTypeName 29} 30 31function hasPreOrPost (item, values) { 32 if (!item.type) return 33 return values[preType(item)] || values[postType(item)] 34} 35 36function generatePreAndPost (baseItem, parentValues) { 37 var item = objectAssign({}, baseItem) 38 var values = Object.create(parentValues) 39 var template = [] 40 var pre = preType(item) 41 var post = postType(item) 42 if (values[pre]) { 43 template.push({value: values[pre]}) 44 values[pre] = null 45 } 46 item.minLength = null 47 item.length = null 48 item.maxLength = null 49 template.push(item) 50 values[item.type] = values[item.type] 51 if (values[post]) { 52 template.push({value: values[post]}) 53 values[post] = null 54 } 55 return function ($1, $2, length) { 56 return renderTemplate(length, template, values) 57 } 58} 59 60function prepareItems (width, template, values) { 61 function cloneAndObjectify (item, index, arr) { 62 var cloned = new TemplateItem(item, width) 63 var type = cloned.type 64 if (cloned.value == null) { 65 if (!(type in values)) { 66 if (cloned.default == null) { 67 throw new error.MissingTemplateValue(cloned, values) 68 } else { 69 cloned.value = cloned.default 70 } 71 } else { 72 cloned.value = values[type] 73 } 74 } 75 if (cloned.value == null || cloned.value === '') return null 76 cloned.index = index 77 cloned.first = index === 0 78 cloned.last = index === arr.length - 1 79 if (hasPreOrPost(cloned, values)) cloned.value = generatePreAndPost(cloned, values) 80 return cloned 81 } 82 83 var output = template.map(cloneAndObjectify).filter(function (item) { return item != null }) 84 85 var outputLength = 0 86 var remainingSpace = width 87 var variableCount = output.length 88 89 function consumeSpace (length) { 90 if (length > remainingSpace) length = remainingSpace 91 outputLength += length 92 remainingSpace -= length 93 } 94 95 function finishSizing (item, length) { 96 if (item.finished) throw new error.Internal('Tried to finish template item that was already finished') 97 if (length === Infinity) throw new error.Internal('Length of template item cannot be infinity') 98 if (length != null) item.length = length 99 item.minLength = null 100 item.maxLength = null 101 --variableCount 102 item.finished = true 103 if (item.length == null) item.length = item.getBaseLength() 104 if (item.length == null) throw new error.Internal('Finished template items must have a length') 105 consumeSpace(item.getLength()) 106 } 107 108 output.forEach(function (item) { 109 if (!item.kerning) return 110 var prevPadRight = item.first ? 0 : output[item.index - 1].padRight 111 if (!item.first && prevPadRight < item.kerning) item.padLeft = item.kerning - prevPadRight 112 if (!item.last) item.padRight = item.kerning 113 }) 114 115 // Finish any that have a fixed (literal or intuited) length 116 output.forEach(function (item) { 117 if (item.getBaseLength() == null) return 118 finishSizing(item) 119 }) 120 121 var resized = 0 122 var resizing 123 var hunkSize 124 do { 125 resizing = false 126 hunkSize = Math.round(remainingSpace / variableCount) 127 output.forEach(function (item) { 128 if (item.finished) return 129 if (!item.maxLength) return 130 if (item.getMaxLength() < hunkSize) { 131 finishSizing(item, item.maxLength) 132 resizing = true 133 } 134 }) 135 } while (resizing && resized++ < output.length) 136 if (resizing) throw new error.Internal('Resize loop iterated too many times while determining maxLength') 137 138 resized = 0 139 do { 140 resizing = false 141 hunkSize = Math.round(remainingSpace / variableCount) 142 output.forEach(function (item) { 143 if (item.finished) return 144 if (!item.minLength) return 145 if (item.getMinLength() >= hunkSize) { 146 finishSizing(item, item.minLength) 147 resizing = true 148 } 149 }) 150 } while (resizing && resized++ < output.length) 151 if (resizing) throw new error.Internal('Resize loop iterated too many times while determining minLength') 152 153 hunkSize = Math.round(remainingSpace / variableCount) 154 output.forEach(function (item) { 155 if (item.finished) return 156 finishSizing(item, hunkSize) 157 }) 158 159 return output 160} 161 162function renderFunction (item, values, length) { 163 validate('OON', arguments) 164 if (item.type) { 165 return item.value(values, values[item.type + 'Theme'] || {}, length) 166 } else { 167 return item.value(values, {}, length) 168 } 169} 170 171function renderValue (item, values) { 172 var length = item.getBaseLength() 173 var value = typeof item.value === 'function' ? renderFunction(item, values, length) : item.value 174 if (value == null || value === '') return '' 175 var alignWith = align[item.align] || align.left 176 var leftPadding = item.padLeft ? align.left('', item.padLeft) : '' 177 var rightPadding = item.padRight ? align.right('', item.padRight) : '' 178 var truncated = wideTruncate(String(value), length) 179 var aligned = alignWith(truncated, length) 180 return leftPadding + aligned + rightPadding 181} 182