@defra/forms-engine-plugin 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.server/server/index.js +0 -4
  2. package/.server/server/index.js.map +1 -1
  3. package/.server/server/plugins/engine/helpers.js +3 -0
  4. package/.server/server/plugins/engine/helpers.js.map +1 -1
  5. package/.server/server/plugins/engine/index.js +27 -1
  6. package/.server/server/plugins/engine/index.js.map +1 -1
  7. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -4
  8. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  9. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +4 -10
  10. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  11. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +2 -3
  12. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  13. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -4
  14. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  15. package/.server/server/plugins/engine/plugin.js +65 -6
  16. package/.server/server/plugins/engine/plugin.js.map +1 -1
  17. package/.server/server/plugins/engine/types.js.map +1 -1
  18. package/.server/server/{views → plugins/engine/views}/components/service-banner/template.test.js +1 -1
  19. package/.server/server/plugins/engine/views/components/service-banner/template.test.js.map +1 -0
  20. package/.server/server/{views → plugins/engine/views}/components/tag-env/template.test.js +1 -1
  21. package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -0
  22. package/.server/server/services/cacheService.js +5 -2
  23. package/.server/server/services/cacheService.js.map +1 -1
  24. package/.server/typings/hapi/index.d.js.map +1 -1
  25. package/README.md +215 -4
  26. package/package.json +3 -3
  27. package/src/client/javascripts/application.js +87 -0
  28. package/src/client/javascripts/file-upload.js +386 -0
  29. package/src/client/stylesheets/_code.scss +33 -0
  30. package/src/client/stylesheets/_govuk-frontend.scss +4 -0
  31. package/src/client/stylesheets/_prose.scss +56 -0
  32. package/src/client/stylesheets/_service-banner.scss +24 -0
  33. package/src/client/stylesheets/_summary-list.scss +28 -0
  34. package/src/client/stylesheets/_tag-env.scss +24 -0
  35. package/src/client/stylesheets/application.scss +14 -0
  36. package/src/common/cookies.js +58 -0
  37. package/src/common/cookies.test.js +23 -0
  38. package/src/common/types.js +5 -0
  39. package/src/config/index.ts +271 -0
  40. package/src/index.ts +31 -0
  41. package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
  42. package/src/server/common/helpers/logging/logger-options.ts +46 -0
  43. package/src/server/common/helpers/logging/logger.ts +7 -0
  44. package/src/server/common/helpers/logging/request-logger.ts +9 -0
  45. package/src/server/common/helpers/logging/request-tracing.js +10 -0
  46. package/src/server/common/helpers/redis-client.js +70 -0
  47. package/src/server/constants.js +1 -0
  48. package/src/server/forms/README.md +10 -0
  49. package/src/server/forms/components.json +1015 -0
  50. package/src/server/forms/report-a-terrorist.json +270 -0
  51. package/src/server/forms/runner-components-test.json +365 -0
  52. package/src/server/forms/test.json +581 -0
  53. package/src/server/index.test.ts +582 -0
  54. package/src/server/index.ts +135 -0
  55. package/src/server/plugins/blankie.test.ts +73 -0
  56. package/src/server/plugins/blankie.ts +48 -0
  57. package/src/server/plugins/crumb.ts +20 -0
  58. package/src/server/plugins/engine/README.md +87 -0
  59. package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
  60. package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
  61. package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
  62. package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
  63. package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
  64. package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
  65. package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
  66. package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
  67. package/src/server/plugins/engine/components/Details.test.ts +49 -0
  68. package/src/server/plugins/engine/components/Details.ts +30 -0
  69. package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
  70. package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
  71. package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
  72. package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
  73. package/src/server/plugins/engine/components/FormComponent.ts +249 -0
  74. package/src/server/plugins/engine/components/Html.test.ts +48 -0
  75. package/src/server/plugins/engine/components/Html.ts +29 -0
  76. package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
  77. package/src/server/plugins/engine/components/InsetText.ts +27 -0
  78. package/src/server/plugins/engine/components/List.test.ts +76 -0
  79. package/src/server/plugins/engine/components/List.ts +72 -0
  80. package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
  81. package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
  82. package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
  83. package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
  84. package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
  85. package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
  86. package/src/server/plugins/engine/components/NumberField.ts +163 -0
  87. package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
  88. package/src/server/plugins/engine/components/RadiosField.ts +24 -0
  89. package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
  90. package/src/server/plugins/engine/components/SelectField.ts +47 -0
  91. package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
  92. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
  93. package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
  94. package/src/server/plugins/engine/components/TextField.test.ts +489 -0
  95. package/src/server/plugins/engine/components/TextField.ts +96 -0
  96. package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
  97. package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
  98. package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
  99. package/src/server/plugins/engine/components/YesNoField.ts +31 -0
  100. package/src/server/plugins/engine/components/constants.ts +1 -0
  101. package/src/server/plugins/engine/components/helpers.ts +330 -0
  102. package/src/server/plugins/engine/components/index.ts +24 -0
  103. package/src/server/plugins/engine/components/types.ts +117 -0
  104. package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
  105. package/src/server/plugins/engine/helpers.test.ts +791 -0
  106. package/src/server/plugins/engine/helpers.ts +384 -0
  107. package/src/server/plugins/engine/index.ts +47 -0
  108. package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
  109. package/src/server/plugins/engine/models/FormModel.ts +443 -0
  110. package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
  111. package/src/server/plugins/engine/models/Section.ts +0 -0
  112. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
  113. package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
  114. package/src/server/plugins/engine/models/index.ts +2 -0
  115. package/src/server/plugins/engine/models/types.ts +114 -0
  116. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
  117. package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
  118. package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
  119. package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
  120. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
  121. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
  122. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
  123. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
  124. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1116 -0
  125. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +447 -0
  126. package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
  127. package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
  128. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
  129. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +565 -0
  130. package/src/server/plugins/engine/pageControllers/README.md +28 -0
  131. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
  132. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
  133. package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
  134. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +51 -0
  135. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +262 -0
  136. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
  137. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
  138. package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
  139. package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
  140. package/src/server/plugins/engine/pageControllers/index.ts +10 -0
  141. package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
  142. package/src/server/plugins/engine/plugin.ts +753 -0
  143. package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
  144. package/src/server/plugins/engine/services/formsService.js +46 -0
  145. package/src/server/plugins/engine/services/formsService.test.js +90 -0
  146. package/src/server/plugins/engine/services/index.js +3 -0
  147. package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
  148. package/src/server/plugins/engine/services/notifyService.ts +64 -0
  149. package/src/server/plugins/engine/services/uploadService.js +60 -0
  150. package/src/server/plugins/engine/types.ts +317 -0
  151. package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
  152. package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
  153. package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
  154. package/src/server/plugins/engine/views/components/debug/macro.njk +3 -0
  155. package/src/server/plugins/engine/views/components/debug/template.njk +13 -0
  156. package/src/server/plugins/engine/views/components/details.html +6 -0
  157. package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
  158. package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
  159. package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
  160. package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
  161. package/src/server/plugins/engine/views/components/html.html +3 -0
  162. package/src/server/plugins/engine/views/components/insettext.html +7 -0
  163. package/src/server/plugins/engine/views/components/list.html +36 -0
  164. package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
  165. package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
  166. package/src/server/plugins/engine/views/components/numberfield.html +5 -0
  167. package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
  168. package/src/server/plugins/engine/views/components/selectfield.html +5 -0
  169. package/src/server/plugins/engine/views/components/service-banner/macro.njk +3 -0
  170. package/src/server/plugins/engine/views/components/service-banner/template.njk +20 -0
  171. package/src/server/plugins/engine/views/components/service-banner/template.test.js +43 -0
  172. package/src/server/plugins/engine/views/components/tag-env/macro.njk +3 -0
  173. package/src/server/plugins/engine/views/components/tag-env/template.njk +30 -0
  174. package/src/server/plugins/engine/views/components/tag-env/template.test.js +66 -0
  175. package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
  176. package/src/server/plugins/engine/views/components/textfield.html +5 -0
  177. package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
  178. package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
  179. package/src/server/plugins/engine/views/confirmation.html +19 -0
  180. package/src/server/plugins/engine/views/file-upload.html +45 -0
  181. package/src/server/plugins/engine/views/index.html +39 -0
  182. package/src/server/plugins/engine/views/item-delete.html +56 -0
  183. package/src/server/plugins/engine/views/layout.html +199 -0
  184. package/src/server/plugins/engine/views/partials/components.html +6 -0
  185. package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
  186. package/src/server/plugins/engine/views/partials/debug.html +44 -0
  187. package/src/server/plugins/engine/views/partials/form.html +15 -0
  188. package/src/server/plugins/engine/views/partials/heading.html +16 -0
  189. package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
  190. package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
  191. package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
  192. package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
  193. package/src/server/plugins/engine/views/summary.html +50 -0
  194. package/src/server/plugins/errorPages.ts +58 -0
  195. package/src/server/plugins/nunjucks/context.js +88 -0
  196. package/src/server/plugins/nunjucks/context.test.js +142 -0
  197. package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
  198. package/src/server/plugins/nunjucks/environment.js +116 -0
  199. package/src/server/plugins/nunjucks/filters/answer.js +27 -0
  200. package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
  201. package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
  202. package/src/server/plugins/nunjucks/filters/field.js +28 -0
  203. package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
  204. package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
  205. package/src/server/plugins/nunjucks/filters/href.js +30 -0
  206. package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
  207. package/src/server/plugins/nunjucks/filters/index.js +8 -0
  208. package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
  209. package/src/server/plugins/nunjucks/filters/page.js +24 -0
  210. package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
  211. package/src/server/plugins/nunjucks/index.js +3 -0
  212. package/src/server/plugins/nunjucks/plugin.js +40 -0
  213. package/src/server/plugins/nunjucks/render.js +42 -0
  214. package/src/server/plugins/nunjucks/types.js +40 -0
  215. package/src/server/plugins/pulse.ts +11 -0
  216. package/src/server/plugins/router.ts +201 -0
  217. package/src/server/plugins/session.ts +28 -0
  218. package/src/server/routes/health.js +13 -0
  219. package/src/server/routes/health.test.js +35 -0
  220. package/src/server/routes/index.test.ts +125 -0
  221. package/src/server/routes/index.ts +2 -0
  222. package/src/server/routes/public.ts +47 -0
  223. package/src/server/routes/types.ts +48 -0
  224. package/src/server/schemas/index.ts +34 -0
  225. package/src/server/secure-context.js +43 -0
  226. package/src/server/services/cacheService.test.ts +277 -0
  227. package/src/server/services/cacheService.ts +138 -0
  228. package/src/server/services/httpService.test.js +491 -0
  229. package/src/server/services/httpService.ts +50 -0
  230. package/src/server/services/index.ts +1 -0
  231. package/src/server/types.ts +54 -0
  232. package/src/server/utils/notify.test.ts +37 -0
  233. package/src/server/utils/notify.ts +50 -0
  234. package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
  235. package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
  236. package/src/server/utils/utils.js +24 -0
  237. package/src/server/utils/utils.test.js +54 -0
  238. package/src/server/views/404.html +16 -0
  239. package/src/server/views/500.html +19 -0
  240. package/src/server/views/help/accessibility-statement.html +58 -0
  241. package/src/server/views/help/cookie-preferences.html +57 -0
  242. package/src/server/views/help/cookies.html +71 -0
  243. package/src/server/views/help/get-support.html +37 -0
  244. package/src/server/views/help/privacy-notice.html +68 -0
  245. package/src/server/views/help/terms-and-conditions.html +83 -0
  246. package/src/typings/hapi/index.d.ts +87 -0
  247. package/src/typings/hapi-tracing/index.d.ts +6 -0
  248. package/src/typings/index.d.ts +3 -0
  249. package/src/typings/joi/index.d.ts +22 -0
  250. package/.server/server/views/components/service-banner/template.test.js.map +0 -1
  251. package/.server/server/views/components/tag-env/template.test.js.map +0 -1
  252. /package/.server/server/{views → plugins/engine/views}/components/debug/macro.njk +0 -0
  253. /package/.server/server/{views → plugins/engine/views}/components/debug/template.njk +0 -0
  254. /package/.server/server/{views → plugins/engine/views}/components/service-banner/macro.njk +0 -0
  255. /package/.server/server/{views → plugins/engine/views}/components/service-banner/template.njk +0 -0
  256. /package/.server/server/{views → plugins/engine/views}/components/tag-env/macro.njk +0 -0
  257. /package/.server/server/{views → plugins/engine/views}/components/tag-env/template.njk +0 -0
  258. /package/.server/server/{views → plugins/engine/views}/confirmation.html +0 -0
  259. /package/.server/server/{views → plugins/engine/views}/layout.html +0 -0
  260. /package/.server/server/{views → plugins/engine/views}/summary.html +0 -0
@@ -0,0 +1,116 @@
1
+ import { dirname, join } from 'node:path'
2
+
3
+ import { ComponentType } from '@defra/forms-model'
4
+ import nunjucks from 'nunjucks'
5
+ import resolvePkg from 'resolve'
6
+
7
+ import { config } from '~/src/config/index.js'
8
+ import { evaluateTemplate } from '~/src/server/plugins/engine/helpers.js'
9
+ import * as filters from '~/src/server/plugins/nunjucks/filters/index.js'
10
+
11
+ const govukFrontendPath = dirname(
12
+ resolvePkg.sync('govuk-frontend/package.json')
13
+ )
14
+
15
+ export const paths = [
16
+ join(config.get('appDir'), 'plugins/engine/views'),
17
+ join(config.get('appDir'), 'views')
18
+ ]
19
+
20
+ export const environment = nunjucks.configure(
21
+ [...paths, join(govukFrontendPath, 'dist')],
22
+ {
23
+ trimBlocks: true,
24
+ lstripBlocks: true,
25
+ watch: config.get('isDevelopment'),
26
+ noCache: config.get('isDevelopment')
27
+ }
28
+ )
29
+
30
+ for (const [name, nunjucksFilter] of Object.entries(filters)) {
31
+ environment.addFilter(name, nunjucksFilter)
32
+ }
33
+
34
+ /**
35
+ * @this {NunjucksContext}
36
+ * @param {FormSubmissionError[]} errors
37
+ */
38
+ export function checkErrorTemplates(errors) {
39
+ const { context } = this.ctx
40
+
41
+ if (!context) {
42
+ return errors
43
+ }
44
+
45
+ errors.forEach((error) => {
46
+ error.text = evaluateTemplate(error.text, context)
47
+ })
48
+
49
+ return errors
50
+ }
51
+
52
+ environment.addGlobal('checkErrorTemplates', checkErrorTemplates)
53
+
54
+ /**
55
+ * @this {NunjucksContext}
56
+ * @param {ComponentViewModel} component
57
+ */
58
+ export function checkComponentTemplates(component) {
59
+ const { context } = this.ctx
60
+
61
+ if (!context) {
62
+ return component
63
+ }
64
+
65
+ if (component.isFormComponent) {
66
+ // Evaluate label/legend text
67
+ if (component.model.fieldset?.legend?.text) {
68
+ const legend = component.model.fieldset.legend
69
+
70
+ legend.text = evaluateTemplate(legend.text, context)
71
+ } else if (component.model.label?.text) {
72
+ const label = component.model.label
73
+
74
+ label.text = evaluateTemplate(label.text, context)
75
+ } else {
76
+ // No template evaluation needed for other component types
77
+ }
78
+
79
+ // Evaluate error message
80
+ if (component.model.errorMessage?.text) {
81
+ const message = component.model.errorMessage
82
+
83
+ message.text = evaluateTemplate(message.text, context)
84
+ }
85
+ } else if (component.type === ComponentType.Html) {
86
+ const content = component.model.content
87
+
88
+ if (typeof content === 'string') {
89
+ component.model.content = evaluateTemplate(content, context)
90
+ }
91
+ } else {
92
+ // No template evaluation needed for other component types
93
+ }
94
+
95
+ return component
96
+ }
97
+
98
+ environment.addGlobal('checkComponentTemplates', checkComponentTemplates)
99
+
100
+ /**
101
+ * @this {NunjucksContext}
102
+ * @param {string} template
103
+ */
104
+ export function evaluate(template) {
105
+ const { context } = this.ctx
106
+
107
+ return context ? evaluateTemplate(template, context) : template
108
+ }
109
+
110
+ environment.addGlobal('evaluate', evaluate)
111
+
112
+ /**
113
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
114
+ * @import { FormSubmissionError } from '~/src/server/plugins/engine/types.js'
115
+ * @import { ComponentViewModel } from '~/src/server/plugins/engine/components/types.js'
116
+ */
@@ -0,0 +1,27 @@
1
+ import { getAnswer } from '~/src/server/plugins/engine/components/helpers.js'
2
+
3
+ /**
4
+ * Nunjucks filter to get the answer for a component
5
+ * @this {NunjucksContext}
6
+ * @param {string} name - The name of the component to check
7
+ */
8
+ export function answer(name) {
9
+ const { context } = this.ctx
10
+
11
+ if (!context) {
12
+ return undefined
13
+ }
14
+
15
+ const component = context.componentMap.get(name)
16
+
17
+ if (!component?.isFormComponent) {
18
+ return undefined
19
+ }
20
+
21
+ return getAnswer(/** @type {Field} */ (component), context.relevantState)
22
+ }
23
+
24
+ /**
25
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
26
+ * @import { Field } from '~/src/server/plugins/engine/components/helpers.js'
27
+ */
@@ -0,0 +1,89 @@
1
+ import { getAnswer } from '~/src/server/plugins/engine/components/helpers.js'
2
+ import { answer } from '~/src/server/plugins/nunjucks/filters/answer.js'
3
+
4
+ jest.mock('~/src/server/plugins/engine/components/helpers.ts', () => ({
5
+ getAnswer: jest.fn()
6
+ }))
7
+
8
+ describe('answer Nunjucks filter', () => {
9
+ /** @type { NunjucksContext } */
10
+ let mockThis
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllMocks()
14
+
15
+ mockThis = /** @type {NunjucksContext} */ (
16
+ /** @type {unknown} */ ({
17
+ ctx: {
18
+ context: {
19
+ componentMap: new Map(),
20
+ relevantState: { someState: 'value' }
21
+ }
22
+ }
23
+ })
24
+ )
25
+
26
+ jest.mocked(getAnswer).mockReturnValue('test answer')
27
+ })
28
+
29
+ describe('missing context', () => {
30
+ it('returns undefined', () => {
31
+ mockThis.ctx.context = undefined
32
+
33
+ const result = answer.call(mockThis, 'componentName')
34
+
35
+ expect(result).toBeUndefined()
36
+ expect(getAnswer).not.toHaveBeenCalled()
37
+ })
38
+ })
39
+
40
+ describe('component lookup', () => {
41
+ it('returns undefined for non-existent component', () => {
42
+ const result = answer.call(mockThis, 'nonExistentComponent')
43
+
44
+ expect(result).toBeUndefined()
45
+ expect(getAnswer).not.toHaveBeenCalled()
46
+ })
47
+ })
48
+
49
+ describe('non-form component', () => {
50
+ it('returns undefined', () => {
51
+ mockThis.ctx.context?.componentMap.set(
52
+ 'nonFormComponent',
53
+ // @ts-expect-error - simplified mock component for testing
54
+ { isFormComponent: false }
55
+ )
56
+
57
+ const result = answer.call(mockThis, 'nonFormComponent')
58
+
59
+ expect(result).toBeUndefined()
60
+ expect(getAnswer).not.toHaveBeenCalled()
61
+ })
62
+ })
63
+
64
+ describe('valid form component', () => {
65
+ it('returns the answer', () => {
66
+ const mockFormComponent = {
67
+ isFormComponent: true,
68
+ someProperty: 'value'
69
+ }
70
+ mockThis.ctx.context?.componentMap.set(
71
+ 'validFormComponent',
72
+ // @ts-expect-error - simplified mock component for testing
73
+ mockFormComponent
74
+ )
75
+
76
+ const result = answer.call(mockThis, 'validFormComponent')
77
+
78
+ expect(getAnswer).toHaveBeenCalledWith(
79
+ mockFormComponent,
80
+ mockThis.ctx.context?.relevantState
81
+ )
82
+ expect(result).toBe('test answer')
83
+ })
84
+ })
85
+ })
86
+
87
+ /**
88
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
89
+ */
@@ -0,0 +1,21 @@
1
+ import { evaluateTemplate } from '~/src/server/plugins/engine/helpers.js'
2
+
3
+ /**
4
+ * Nunjucks filter to evaluate a liquid template.
5
+ * Currently just used in `src/server/views/layout.html#LN37` for the pageTitle
6
+ * @this {NunjucksContext}
7
+ * @param {string} template
8
+ */
9
+ export function evaluate(template) {
10
+ const { context } = this.ctx
11
+
12
+ if (!context || typeof template !== 'string') {
13
+ return template
14
+ }
15
+
16
+ return evaluateTemplate(template, context)
17
+ }
18
+
19
+ /**
20
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
21
+ */
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Nunjucks filter to get the answer for a component
3
+ * @this {NunjucksContext}
4
+ * @param {string} name - The name of the component
5
+ */
6
+ export function field(name) {
7
+ if (typeof name !== 'string') {
8
+ return undefined
9
+ }
10
+
11
+ const { context } = this.ctx
12
+
13
+ if (!context) {
14
+ return undefined
15
+ }
16
+
17
+ const component = context.componentMap.get(name)
18
+
19
+ if (!component?.isFormComponent) {
20
+ return undefined
21
+ }
22
+
23
+ return component
24
+ }
25
+
26
+ /**
27
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
28
+ */
@@ -0,0 +1,75 @@
1
+ import { field } from '~/src/server/plugins/nunjucks/filters/field.js'
2
+
3
+ describe('field Nunjucks filter', () => {
4
+ /** @type {NunjucksContext} */
5
+ let mockThis
6
+
7
+ beforeEach(() => {
8
+ mockThis = /** @type {NunjucksContext} */ (
9
+ /** @type {unknown} */ ({
10
+ ctx: {
11
+ context: {
12
+ componentMap: new Map()
13
+ }
14
+ }
15
+ })
16
+ )
17
+ })
18
+
19
+ describe('invalid inputs', () => {
20
+ it('returns undefined for non-string name parameter', () => {
21
+ // @ts-expect-error - testing invalid input
22
+ const result = field.call(mockThis, 123)
23
+ expect(result).toBeUndefined()
24
+ })
25
+ })
26
+
27
+ describe('missing context', () => {
28
+ it('returns undefined', () => {
29
+ mockThis.ctx.context = undefined
30
+ const result = field.call(mockThis, 'componentName')
31
+ expect(result).toBeUndefined()
32
+ })
33
+ })
34
+
35
+ describe('component lookup', () => {
36
+ it('returns undefined for non-existent component', () => {
37
+ const result = field.call(mockThis, 'nonExistentComponent')
38
+ expect(result).toBeUndefined()
39
+ })
40
+ })
41
+
42
+ describe('non-form component', () => {
43
+ it('returns undefined', () => {
44
+ mockThis.ctx.context?.componentMap.set(
45
+ 'nonFormComponent',
46
+ // @ts-expect-error - simplified mock component for testing
47
+ { isFormComponent: false }
48
+ )
49
+
50
+ const result = field.call(mockThis, 'nonFormComponent')
51
+ expect(result).toBeUndefined()
52
+ })
53
+ })
54
+
55
+ describe('valid form component', () => {
56
+ it('returns the component', () => {
57
+ const mockFormComponent = {
58
+ isFormComponent: true,
59
+ someProperty: 'value'
60
+ }
61
+ mockThis.ctx.context?.componentMap.set(
62
+ 'validFormComponent',
63
+ // @ts-expect-error - simplified mock component for testing
64
+ mockFormComponent
65
+ )
66
+
67
+ const result = field.call(mockThis, 'validFormComponent')
68
+ expect(result).toBe(mockFormComponent)
69
+ })
70
+ })
71
+ })
72
+
73
+ /**
74
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
75
+ */
@@ -0,0 +1,11 @@
1
+ import hljs from 'highlight.js'
2
+
3
+ /**
4
+ * Format code with syntax highlighting
5
+ * @param {string} code - Code in plain text
6
+ * @param {string} [language] - Code programming language
7
+ * @returns {string} Code with syntax highlighting
8
+ */
9
+ export function highlight(code, language) {
10
+ return hljs.highlight(code, { language: language ?? 'plaintext' }).value
11
+ }
@@ -0,0 +1,30 @@
1
+ import { getPageHref } from '~/src/server/plugins/engine/index.js'
2
+
3
+ /**
4
+ * Nunjucks filter to get the answer for a component
5
+ * @this {NunjucksContext}
6
+ * @param {string} path - The name of the component
7
+ */
8
+ export function href(path) {
9
+ if (typeof path !== 'string') {
10
+ return undefined
11
+ }
12
+
13
+ const { context } = this.ctx
14
+
15
+ if (!context) {
16
+ return undefined
17
+ }
18
+
19
+ const page = context.pageMap.get(path)
20
+
21
+ if (page === undefined) {
22
+ return undefined
23
+ }
24
+
25
+ return getPageHref(page)
26
+ }
27
+
28
+ /**
29
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
30
+ */
@@ -0,0 +1,80 @@
1
+ import { getPageHref } from '~/src/server/plugins/engine/index.js'
2
+ import { href } from '~/src/server/plugins/nunjucks/filters/href.js'
3
+
4
+ jest.mock('~/src/server/plugins/engine/index.ts', () => ({
5
+ getPageHref: jest.fn()
6
+ }))
7
+
8
+ describe('href Nunjucks filter', () => {
9
+ /** @type { NunjucksContext } */
10
+ let mockThis
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllMocks()
14
+
15
+ mockThis = /** @type {NunjucksContext} */ (
16
+ /** @type {unknown} */ ({
17
+ ctx: {
18
+ context: {
19
+ pageMap: new Map()
20
+ }
21
+ }
22
+ })
23
+ )
24
+
25
+ jest.mocked(getPageHref).mockReturnValue('/some-page')
26
+ })
27
+
28
+ describe('invalid inputs', () => {
29
+ it('returns undefined for non-string path parameter', () => {
30
+ // @ts-expect-error - testing invalid input
31
+ const result = href.call(mockThis, 123)
32
+
33
+ expect(result).toBeUndefined()
34
+ expect(getPageHref).not.toHaveBeenCalled()
35
+ })
36
+ })
37
+
38
+ describe('missing context', () => {
39
+ it('returns undefined', () => {
40
+ mockThis.ctx.context = undefined
41
+
42
+ const result = href.call(mockThis, 'pagePath')
43
+
44
+ expect(result).toBeUndefined()
45
+ expect(getPageHref).not.toHaveBeenCalled()
46
+ })
47
+ })
48
+
49
+ describe('page lookup', () => {
50
+ it('returns undefined for non-existent page', () => {
51
+ const result = href.call(mockThis, 'nonExistentPage')
52
+
53
+ expect(result).toBeUndefined()
54
+ expect(getPageHref).not.toHaveBeenCalled()
55
+ })
56
+ })
57
+
58
+ describe('valid page', () => {
59
+ it('returns the href for the page', () => {
60
+ const mockPage = {
61
+ path: '/some-page',
62
+ someProperty: 'value'
63
+ }
64
+ mockThis.ctx.context?.pageMap.set(
65
+ 'validPage',
66
+ // @ts-expect-error - simplified mock page for testing
67
+ mockPage
68
+ )
69
+
70
+ const result = href.call(mockThis, 'validPage')
71
+
72
+ expect(getPageHref).toHaveBeenCalledWith(mockPage)
73
+ expect(result).toBe('/some-page')
74
+ })
75
+ })
76
+ })
77
+
78
+ /**
79
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
80
+ */
@@ -0,0 +1,8 @@
1
+ export { markdownToHtml as markdown } from '@defra/forms-model'
2
+ export { highlight } from '~/src/server/plugins/nunjucks/filters/highlight.js'
3
+ export { inspect } from '~/src/server/plugins/nunjucks/filters/inspect.js'
4
+ export { evaluate } from '~/src/server/plugins/nunjucks/filters/evaluate.js'
5
+ export { answer } from '~/src/server/plugins/nunjucks/filters/answer.js'
6
+ export { href } from '~/src/server/plugins/nunjucks/filters/href.js'
7
+ export { field } from '~/src/server/plugins/nunjucks/filters/field.js'
8
+ export { page } from '~/src/server/plugins/nunjucks/filters/page.js'
@@ -0,0 +1,15 @@
1
+ import util from 'util'
2
+
3
+ /**
4
+ * Format JavaScript objects as strings
5
+ * @param {unknown} object - JavaScript object to format
6
+ * @returns {string} Formatted string
7
+ */
8
+ export function inspect(object) {
9
+ return util.inspect(object, {
10
+ compact: false,
11
+ depth: Infinity,
12
+ maxArrayLength: Infinity,
13
+ maxStringLength: Infinity
14
+ })
15
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Nunjucks filter to get the page for a given path
3
+ * @this {NunjucksContext}
4
+ * @param {string} path - The path of the page
5
+ */
6
+ export function page(path) {
7
+ if (typeof path !== 'string') {
8
+ return undefined
9
+ }
10
+
11
+ const { context } = this.ctx
12
+
13
+ if (!context) {
14
+ return undefined
15
+ }
16
+
17
+ const foundPage = context.pageMap.get(path)
18
+
19
+ return foundPage
20
+ }
21
+
22
+ /**
23
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
24
+ */
@@ -0,0 +1,65 @@
1
+ import { page } from '~/src/server/plugins/nunjucks/filters/page.js'
2
+
3
+ describe('page Nunjucks filter', () => {
4
+ /** @type { NunjucksContext } */
5
+ let mockThis
6
+
7
+ beforeEach(() => {
8
+ mockThis = /** @type {NunjucksContext} */ (
9
+ /** @type {unknown} */ ({
10
+ ctx: {
11
+ context: {
12
+ pageMap: new Map()
13
+ }
14
+ }
15
+ })
16
+ )
17
+ })
18
+
19
+ describe('invalid inputs', () => {
20
+ it('returns undefined for non-string path parameter', () => {
21
+ // @ts-expect-error - testing invalid input
22
+ const result = page.call(mockThis, 123)
23
+
24
+ expect(result).toBeUndefined()
25
+ })
26
+ })
27
+
28
+ describe('missing context', () => {
29
+ it('returns undefined', () => {
30
+ mockThis.ctx.context = undefined
31
+
32
+ const result = page.call(mockThis, 'pagePath')
33
+
34
+ expect(result).toBeUndefined()
35
+ })
36
+ })
37
+
38
+ describe('page lookup', () => {
39
+ it('returns undefined for non-existent page', () => {
40
+ const result = page.call(mockThis, 'nonExistentPage')
41
+
42
+ expect(result).toBeUndefined()
43
+ })
44
+
45
+ it('returns the page when found', () => {
46
+ const mockPage = {
47
+ path: '/some-page',
48
+ someProperty: 'value'
49
+ }
50
+ mockThis.ctx.context?.pageMap.set(
51
+ 'validPage',
52
+ // @ts-expect-error - simplified mock page for testing
53
+ mockPage
54
+ )
55
+
56
+ const result = page.call(mockThis, 'validPage')
57
+
58
+ expect(result).toBe(mockPage)
59
+ })
60
+ })
61
+ })
62
+
63
+ /**
64
+ * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
65
+ */
@@ -0,0 +1,3 @@
1
+ export * as render from '~/src/server/plugins/nunjucks/render.js'
2
+ export { environment } from '~/src/server/plugins/nunjucks/environment.js'
3
+ export { plugin } from '~/src/server/plugins/nunjucks/plugin.js'
@@ -0,0 +1,40 @@
1
+ import vision from '@hapi/vision'
2
+ import nunjucks from 'nunjucks'
3
+
4
+ import { config } from '~/src/config/index.js'
5
+ import { context } from '~/src/server/plugins/nunjucks/context.js'
6
+ import {
7
+ environment,
8
+ paths
9
+ } from '~/src/server/plugins/nunjucks/environment.js'
10
+
11
+ /**
12
+ * @type {ServerRegisterPluginObject<ServerViewsConfiguration>}
13
+ */
14
+ export const plugin = {
15
+ plugin: vision,
16
+ options: {
17
+ engines: {
18
+ html: {
19
+ /**
20
+ * @param {string} path
21
+ * @param {{ environment: typeof environment }} compileOptions
22
+ * @returns {(options: ReturnType<Awaited<typeof context>>) => string}
23
+ */
24
+ compile(path, compileOptions) {
25
+ return (options) =>
26
+ nunjucks.compile(path, compileOptions.environment).render(options)
27
+ }
28
+ }
29
+ },
30
+ path: paths,
31
+ compileOptions: { environment },
32
+ isCached: config.get('isProduction'),
33
+ context
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @import { ServerRegisterPluginObject } from '@hapi/hapi'
39
+ * @import { ServerViewsConfiguration } from '@hapi/vision'
40
+ */
@@ -0,0 +1,42 @@
1
+ import { environment } from '~/src/server/plugins/nunjucks/environment.js'
2
+
3
+ /**
4
+ * Render Nunjucks macro
5
+ * @param {string} macroName
6
+ * @param {string} macroPath
7
+ * @param {RenderOptions & MacroOptions} [options]
8
+ */
9
+ export function macro(macroName, macroPath, options) {
10
+ const macroParams = JSON.stringify(options?.params, undefined, 2)
11
+ let macroString = `{%- from "${macroPath}" import ${macroName} -%}`
12
+
13
+ if (options?.callBlock) {
14
+ macroString += `{%- call ${macroName}(${macroParams}) -%}${options.callBlock}{%- endcall -%}`
15
+ } else {
16
+ macroString += `{{- ${macroName}(${macroParams}) -}}`
17
+ }
18
+
19
+ return string(macroString, options)
20
+ }
21
+
22
+ /**
23
+ * Render Nunjucks code
24
+ * @param {string} viewString - Nunjucks string to render
25
+ * @param {RenderOptions} [options]
26
+ */
27
+ export function string(viewString, options) {
28
+ return environment.renderString(viewString, options?.context ?? {})
29
+ }
30
+
31
+ /**
32
+ * Render Nunjucks view
33
+ * @param {string} viewPath
34
+ * @param {RenderOptions} [options]
35
+ */
36
+ export function view(viewPath, options) {
37
+ return environment.render(viewPath, options?.context)
38
+ }
39
+
40
+ /**
41
+ * @import { MacroOptions, RenderOptions } from '~/src/server/plugins/nunjucks/types.js'
42
+ */