render-template.js 5.6 KB

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