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