You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

render-template.js 5.8KB

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