• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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