odac 0.9.0

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 (213) hide show
  1. package/.editorconfig +21 -0
  2. package/.github/workflows/auto-pr-description.yml +49 -0
  3. package/.github/workflows/release.yml +32 -0
  4. package/.github/workflows/test-coverage.yml +58 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.kiro/steering/code-style.md +56 -0
  7. package/.kiro/steering/product.md +20 -0
  8. package/.kiro/steering/structure.md +77 -0
  9. package/.kiro/steering/tech.md +87 -0
  10. package/.prettierrc +10 -0
  11. package/.releaserc.js +134 -0
  12. package/AGENTS.md +84 -0
  13. package/CHANGELOG.md +181 -0
  14. package/CODE_OF_CONDUCT.md +83 -0
  15. package/CONTRIBUTING.md +63 -0
  16. package/LICENSE +661 -0
  17. package/README.md +57 -0
  18. package/SECURITY.md +26 -0
  19. package/bin/candy +10 -0
  20. package/bin/candypack +10 -0
  21. package/cli/index.js +3 -0
  22. package/cli/src/Cli.js +348 -0
  23. package/cli/src/Connector.js +93 -0
  24. package/cli/src/Monitor.js +416 -0
  25. package/core/Candy.js +87 -0
  26. package/core/Commands.js +239 -0
  27. package/core/Config.js +1094 -0
  28. package/core/Lang.js +52 -0
  29. package/core/Log.js +43 -0
  30. package/core/Process.js +26 -0
  31. package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
  32. package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
  33. package/docs/backend/01-overview/03-development-server.md +79 -0
  34. package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
  35. package/docs/backend/03-config/00-configuration-overview.md +214 -0
  36. package/docs/backend/03-config/01-database-connection.md +60 -0
  37. package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
  38. package/docs/backend/03-config/03-request-timeout.md +11 -0
  39. package/docs/backend/03-config/04-environment-variables.md +227 -0
  40. package/docs/backend/03-config/05-early-hints.md +352 -0
  41. package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
  42. package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
  43. package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
  44. package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
  45. package/docs/backend/04-routing/05-advanced-routing.md +14 -0
  46. package/docs/backend/04-routing/06-error-pages.md +101 -0
  47. package/docs/backend/04-routing/07-cron-jobs.md +149 -0
  48. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
  49. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
  50. package/docs/backend/05-controllers/03-controller-classes.md +93 -0
  51. package/docs/backend/05-forms/01-custom-forms.md +395 -0
  52. package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
  53. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
  54. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
  55. package/docs/backend/07-views/01-the-view-directory.md +73 -0
  56. package/docs/backend/07-views/02-rendering-a-view.md +179 -0
  57. package/docs/backend/07-views/03-template-syntax.md +181 -0
  58. package/docs/backend/07-views/03-variables.md +328 -0
  59. package/docs/backend/07-views/04-request-data.md +231 -0
  60. package/docs/backend/07-views/05-conditionals.md +290 -0
  61. package/docs/backend/07-views/06-loops.md +353 -0
  62. package/docs/backend/07-views/07-translations.md +358 -0
  63. package/docs/backend/07-views/08-backend-javascript.md +398 -0
  64. package/docs/backend/07-views/09-comments.md +297 -0
  65. package/docs/backend/08-database/01-database-connection.md +99 -0
  66. package/docs/backend/08-database/02-using-mysql.md +322 -0
  67. package/docs/backend/09-validation/01-the-validator-service.md +424 -0
  68. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
  69. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
  70. package/docs/backend/10-authentication/03-register.md +134 -0
  71. package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
  72. package/docs/backend/10-authentication/05-session-management.md +159 -0
  73. package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
  74. package/docs/backend/11-mail/01-the-mail-service.md +42 -0
  75. package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
  76. package/docs/backend/13-utilities/01-candy-var.md +504 -0
  77. package/docs/frontend/01-overview/01-introduction.md +146 -0
  78. package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
  79. package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
  80. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
  81. package/docs/frontend/03-forms/01-form-handling.md +420 -0
  82. package/docs/frontend/04-api-requests/01-get-post.md +443 -0
  83. package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
  84. package/docs/index.json +452 -0
  85. package/docs/server/01-installation/01-quick-install.md +19 -0
  86. package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
  87. package/docs/server/02-get-started/01-core-concepts.md +7 -0
  88. package/docs/server/02-get-started/02-basic-commands.md +57 -0
  89. package/docs/server/02-get-started/03-cli-reference.md +276 -0
  90. package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
  91. package/docs/server/03-service/01-start-a-new-service.md +57 -0
  92. package/docs/server/03-service/02-delete-a-service.md +48 -0
  93. package/docs/server/04-web/01-create-a-website.md +36 -0
  94. package/docs/server/04-web/02-list-websites.md +9 -0
  95. package/docs/server/04-web/03-delete-a-website.md +29 -0
  96. package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
  97. package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
  98. package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
  99. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
  100. package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
  101. package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
  102. package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
  103. package/docs/server/07-mail/04-change-account-password.md +23 -0
  104. package/eslint.config.mjs +120 -0
  105. package/framework/index.js +4 -0
  106. package/framework/src/Auth.js +309 -0
  107. package/framework/src/Candy.js +81 -0
  108. package/framework/src/Config.js +79 -0
  109. package/framework/src/Env.js +60 -0
  110. package/framework/src/Lang.js +57 -0
  111. package/framework/src/Mail.js +83 -0
  112. package/framework/src/Mysql.js +575 -0
  113. package/framework/src/Request.js +301 -0
  114. package/framework/src/Route/Cron.js +128 -0
  115. package/framework/src/Route/Internal.js +439 -0
  116. package/framework/src/Route.js +455 -0
  117. package/framework/src/Server.js +15 -0
  118. package/framework/src/Stream.js +163 -0
  119. package/framework/src/Token.js +37 -0
  120. package/framework/src/Validator.js +271 -0
  121. package/framework/src/Var.js +211 -0
  122. package/framework/src/View/EarlyHints.js +190 -0
  123. package/framework/src/View/Form.js +600 -0
  124. package/framework/src/View.js +513 -0
  125. package/framework/web/candy.js +838 -0
  126. package/jest.config.js +22 -0
  127. package/locale/de-DE.json +80 -0
  128. package/locale/en-US.json +79 -0
  129. package/locale/es-ES.json +80 -0
  130. package/locale/fr-FR.json +80 -0
  131. package/locale/pt-BR.json +80 -0
  132. package/locale/ru-RU.json +80 -0
  133. package/locale/tr-TR.json +85 -0
  134. package/locale/zh-CN.json +80 -0
  135. package/package.json +86 -0
  136. package/server/index.js +5 -0
  137. package/server/src/Api.js +88 -0
  138. package/server/src/DNS.js +940 -0
  139. package/server/src/Hub.js +535 -0
  140. package/server/src/Mail.js +571 -0
  141. package/server/src/SSL.js +180 -0
  142. package/server/src/Server.js +27 -0
  143. package/server/src/Service.js +248 -0
  144. package/server/src/Subdomain.js +64 -0
  145. package/server/src/Web/Firewall.js +170 -0
  146. package/server/src/Web/Proxy.js +134 -0
  147. package/server/src/Web.js +451 -0
  148. package/server/src/mail/imap.js +1091 -0
  149. package/server/src/mail/server.js +32 -0
  150. package/server/src/mail/smtp.js +786 -0
  151. package/test/cli/Cli.test.js +36 -0
  152. package/test/core/Candy.test.js +234 -0
  153. package/test/core/Commands.test.js +538 -0
  154. package/test/core/Config.test.js +1435 -0
  155. package/test/core/Lang.test.js +250 -0
  156. package/test/core/Process.test.js +156 -0
  157. package/test/framework/Route.test.js +239 -0
  158. package/test/framework/View/EarlyHints.test.js +282 -0
  159. package/test/scripts/check-coverage.js +132 -0
  160. package/test/server/Api.test.js +647 -0
  161. package/test/server/Client.test.js +338 -0
  162. package/test/server/DNS.test.js +2050 -0
  163. package/test/server/DNS.test.js.bak +2084 -0
  164. package/test/server/Log.test.js +73 -0
  165. package/test/server/Mail.account.test_.js +460 -0
  166. package/test/server/Mail.init.test_.js +411 -0
  167. package/test/server/Mail.test_.js +1340 -0
  168. package/test/server/SSL.test_.js +1491 -0
  169. package/test/server/Server.test.js +765 -0
  170. package/test/server/Service.test_.js +1127 -0
  171. package/test/server/Subdomain.test.js +440 -0
  172. package/test/server/Web/Firewall.test.js +175 -0
  173. package/test/server/Web.test_.js +1562 -0
  174. package/test/server/__mocks__/acme-client.js +17 -0
  175. package/test/server/__mocks__/bcrypt.js +50 -0
  176. package/test/server/__mocks__/child_process.js +389 -0
  177. package/test/server/__mocks__/crypto.js +432 -0
  178. package/test/server/__mocks__/fs.js +450 -0
  179. package/test/server/__mocks__/globalCandy.js +227 -0
  180. package/test/server/__mocks__/http-proxy.js +105 -0
  181. package/test/server/__mocks__/http.js +575 -0
  182. package/test/server/__mocks__/https.js +272 -0
  183. package/test/server/__mocks__/index.js +249 -0
  184. package/test/server/__mocks__/mail/server.js +100 -0
  185. package/test/server/__mocks__/mail/smtp.js +31 -0
  186. package/test/server/__mocks__/mailparser.js +81 -0
  187. package/test/server/__mocks__/net.js +369 -0
  188. package/test/server/__mocks__/node-forge.js +328 -0
  189. package/test/server/__mocks__/os.js +320 -0
  190. package/test/server/__mocks__/path.js +291 -0
  191. package/test/server/__mocks__/selfsigned.js +8 -0
  192. package/test/server/__mocks__/server/src/mail/server.js +100 -0
  193. package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
  194. package/test/server/__mocks__/smtp-server.js +106 -0
  195. package/test/server/__mocks__/sqlite3.js +394 -0
  196. package/test/server/__mocks__/testFactories.js +299 -0
  197. package/test/server/__mocks__/testHelpers.js +363 -0
  198. package/test/server/__mocks__/tls.js +229 -0
  199. package/watchdog/index.js +3 -0
  200. package/watchdog/src/Watchdog.js +156 -0
  201. package/web/config.json +5 -0
  202. package/web/controller/page/about.js +27 -0
  203. package/web/controller/page/index.js +34 -0
  204. package/web/package.json +18 -0
  205. package/web/public/assets/css/style.css +1835 -0
  206. package/web/public/assets/js/app.js +96 -0
  207. package/web/route/www.js +19 -0
  208. package/web/skeleton/main.html +22 -0
  209. package/web/view/content/about.html +65 -0
  210. package/web/view/content/home.html +205 -0
  211. package/web/view/footer/main.html +11 -0
  212. package/web/view/head/main.html +5 -0
  213. package/web/view/header/main.html +14 -0
@@ -0,0 +1,513 @@
1
+ const nodeCrypto = require('crypto')
2
+ const fs = require('fs')
3
+ const Form = require('./View/Form')
4
+ const EarlyHints = require('./View/EarlyHints')
5
+
6
+ const CACHE_DIR = './storage/.cache'
7
+
8
+ class View {
9
+ #cache = {}
10
+ #earlyHints = null
11
+ #functions = {
12
+ '{!!': {
13
+ function: '${await ',
14
+ close: '!!}',
15
+ end: '}'
16
+ },
17
+ '{{--': {
18
+ function: '`; /*',
19
+ close: '--}}',
20
+ end: '*/ html += `'
21
+ },
22
+ '{{': {
23
+ function: '${Candy.Var(await ',
24
+ close: '}}',
25
+ end: ').html().replace(/\\n/g, "<br>")}'
26
+ },
27
+ break: {
28
+ function: 'break;',
29
+ arguments: {}
30
+ },
31
+ component: {
32
+ // TODO: Implement component
33
+ // <candy:component name="navbar" title="Dashboard"/>
34
+ },
35
+ continue: {
36
+ function: 'continue;',
37
+ arguments: {}
38
+ },
39
+ mysql: {
40
+ // TODO: Implement mysql
41
+ },
42
+ elseif: {
43
+ function: '} else if(await ($condition)){',
44
+ arguments: {
45
+ condition: true
46
+ }
47
+ },
48
+ else: {
49
+ function: '} else {'
50
+ },
51
+ fetch: {
52
+ // TODO: Implement fetch
53
+ // <candy:fetch fetch="/get/products" as="data" method="GET" headers="{}" body="null" refresh="false">
54
+ },
55
+ for: {
56
+ function: '{ let _arr = $constructor; for(let $key in _arr){ let $value = _arr[$key];',
57
+ end: '}}',
58
+ arguments: {
59
+ var: null,
60
+ get: null,
61
+ key: 'key',
62
+ value: 'value'
63
+ }
64
+ },
65
+ if: {
66
+ function: 'if(await ($condition)){',
67
+ arguments: {
68
+ condition: true
69
+ }
70
+ },
71
+ '<candy:js>': {
72
+ end: ' html += `',
73
+ function: '`; ',
74
+ close: '</candy:js>'
75
+ },
76
+ lazy: {
77
+ // TODO: Implement lazy
78
+ // <candy:lazy>
79
+ // <candy:component name="profile-card" data="user"/>
80
+ // </candy:lazy>
81
+ },
82
+ list: {
83
+ arguments: {
84
+ var: null,
85
+ get: null,
86
+ key: 'key',
87
+ value: 'value'
88
+ },
89
+ end: '}}',
90
+ function: '{ let _arr = $constructor; for(let $key in _arr){ let $value = _arr[$key];',
91
+ replace: 'ul'
92
+ },
93
+ while: {
94
+ function: 'while(await ($condition)){',
95
+ arguments: {
96
+ condition: true
97
+ }
98
+ }
99
+ }
100
+ #part = {}
101
+ #candy = null
102
+
103
+ constructor(candy) {
104
+ this.#candy = candy
105
+
106
+ if (!global.Candy?.View?.EarlyHints) {
107
+ const config = candy.Config?.earlyHints
108
+ this.#earlyHints = new EarlyHints(config)
109
+ this.#earlyHints.init()
110
+
111
+ if (!global.Candy) global.Candy = {}
112
+ if (!global.Candy.View) global.Candy.View = {}
113
+ global.Candy.View.EarlyHints = this.#earlyHints
114
+ } else {
115
+ this.#earlyHints = global.Candy.View.EarlyHints
116
+ }
117
+ }
118
+
119
+ all(name) {
120
+ this.#part.all = name
121
+ return this
122
+ }
123
+
124
+ // - PRINT VIEW
125
+ async print() {
126
+ if (this.#candy.Request.res.finished) return
127
+
128
+ const routePath = this.#candy.Request.req.url.split('?')[0]
129
+
130
+ // Handle AJAX load requests
131
+ if (this.#candy.Request.isAjaxLoad === true && this.#candy.Request.ajaxLoad && this.#candy.Request.ajaxLoad.length > 0) {
132
+ let output = {}
133
+ let variables = {}
134
+
135
+ // Collect variables marked for AJAX
136
+ for (let key in this.#candy.Request.variables) {
137
+ if (this.#candy.Request.variables[key].ajax) {
138
+ variables[key] = this.#candy.Request.variables[key].value
139
+ }
140
+ }
141
+
142
+ // Render requested elements
143
+ for (let element of this.#candy.Request.ajaxLoad) {
144
+ if (this.#part[element]) {
145
+ let viewPath = this.#part[element]
146
+ if (viewPath.includes('.')) viewPath = viewPath.replace(/\./g, '/')
147
+ if (fs.existsSync(`./view/${element}/${viewPath}.html`)) {
148
+ output[element] = await this.#render(`./view/${element}/${viewPath}.html`)
149
+ }
150
+ }
151
+ }
152
+
153
+ const currentSkeleton = this.#part.skeleton || 'main'
154
+ const clientSkeleton = this.#candy.Request.clientSkeleton
155
+ const skeletonChanged = clientSkeleton && clientSkeleton !== currentSkeleton
156
+
157
+ this.#candy.Request.header('Content-Type', 'application/json')
158
+ this.#candy.Request.header('X-Candy-Page', this.#candy.Request.page || '')
159
+ this.#candy.Request.header('Vary', 'X-Candy')
160
+
161
+ this.#candy.Request.end({
162
+ output: output,
163
+ variables: variables,
164
+ skeletonChanged: skeletonChanged
165
+ })
166
+ return
167
+ }
168
+
169
+ // Normal page rendering
170
+ let result = ''
171
+ if (this.#part.skeleton && fs.existsSync(`./skeleton/${this.#part.skeleton}.html`)) {
172
+ result = fs.readFileSync(`./skeleton/${this.#part.skeleton}.html`, 'utf8')
173
+
174
+ // Add data-candy-navigate to content wrapper for auto-navigation
175
+ result = this.#addNavigateAttribute(result)
176
+
177
+ for (let key in this.#part) {
178
+ if (['all', 'skeleton'].includes(key)) continue
179
+ if (!this.#part[key]) continue
180
+ if (this.#part[key].includes('.')) this.#part[key] = this.#part[key].replace(/\./g, '/')
181
+ if (fs.existsSync(`./view/${key}/${this.#part[key]}.html`)) {
182
+ result = result.replace(`{{ ${key.toUpperCase()} }}`, await this.#render(`./view/${key}/${this.#part[key]}.html`))
183
+ }
184
+ }
185
+ if (this.#part.all) {
186
+ let parts = result.match(/{{.*?}}/g).map(part => part.replace(/{{|}}/g, '').trim())
187
+ if (parts)
188
+ for (let part of parts) {
189
+ part = part.trim()
190
+ let file = this.#part.all.split('.')
191
+ file.splice(-1, 0, part.toLowerCase())
192
+ file = file.join('/')
193
+ if (fs.existsSync(`./view/${file}.html`)) {
194
+ result = result.replace(`{{ ${part.toUpperCase()} }}`, await this.#render(`./view/${file}.html`))
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ if (result) {
201
+ const hasEarlyHints = this.#candy.Request.hasEarlyHints()
202
+
203
+ if (!hasEarlyHints) {
204
+ const detectedResources = this.#earlyHints.extractFromHtml(result)
205
+
206
+ if (detectedResources && detectedResources.length > 0) {
207
+ this.#earlyHints.cacheHints(routePath, detectedResources)
208
+ }
209
+ }
210
+ }
211
+
212
+ this.#candy.Request.header('Content-Type', 'text/html')
213
+ this.#candy.Request.end(result)
214
+ }
215
+
216
+ #parseCandyTag(content) {
217
+ // Parse backend comments
218
+ // Multi-line: <!--candy ... candy-->
219
+ // Single-line: <!--candy ... -->
220
+ content = content.replace(/<!--candy([\s\S]*?)(?:candy-->|-->)/g, () => {
221
+ return ''
222
+ })
223
+
224
+ // Parse <script:candy> tags (IDE-friendly JavaScript with backend execution)
225
+ content = content.replace(/<script:candy([^>]*)>([\s\S]*?)<\/script:candy>/g, (fullMatch, attributes, jsContent) => {
226
+ return `<candy:js>${jsContent}</candy:js>`
227
+ })
228
+
229
+ content = content.replace(/<candy:else\s*\/>/g, '<candy:else>')
230
+ content = content.replace(/<candy:elseif\s+([^>]*?)\/>/g, '<candy:elseif $1>')
231
+
232
+ content = content.replace(/<candy([^>]*?)\/>/g, (fullMatch, attributes) => {
233
+ attributes = attributes.trim()
234
+
235
+ const attrs = {}
236
+ const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
237
+ let match
238
+ while ((match = attrRegex.exec(attributes))) {
239
+ const key = match[1]
240
+ const value = match[3] !== undefined ? match[3] : match[4] !== undefined ? match[4] : true
241
+ attrs[key] = value
242
+ }
243
+
244
+ if (attrs.get) {
245
+ return `{{ get('${attrs.get}') || '' }}`
246
+ } else if (attrs.var) {
247
+ if (attrs.raw) {
248
+ return `{!! ${attrs.var} !!}`
249
+ } else {
250
+ return `{{ ${attrs.var} }}`
251
+ }
252
+ }
253
+ return fullMatch
254
+ })
255
+
256
+ let depth = 0
257
+ let maxDepth = 10
258
+ while (depth < maxDepth && content.includes('<candy')) {
259
+ const before = content
260
+ content = content.replace(/<candy([^>]*)>((?:(?!<candy)[\s\S])*?)<\/candy>/g, (fullMatch, attributes, innerContent) => {
261
+ attributes = attributes.trim()
262
+ innerContent = innerContent.trim()
263
+
264
+ const attrs = {}
265
+ const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
266
+ let match
267
+ while ((match = attrRegex.exec(attributes))) {
268
+ const key = match[1]
269
+ const value = match[3] !== undefined ? match[3] : match[4] !== undefined ? match[4] : true
270
+ attrs[key] = value
271
+ }
272
+
273
+ if (attrs.get) {
274
+ return `{{ get('${attrs.get}') || '' }}`
275
+ } else if (attrs.var) {
276
+ if (attrs.raw) {
277
+ return `{!! ${attrs.var} !!}`
278
+ } else {
279
+ return `{{ ${attrs.var} }}`
280
+ }
281
+ } else if (attrs.t || attrs.translate) {
282
+ const placeholders = []
283
+ let processedContent = innerContent
284
+ let placeholderIndex = 1
285
+
286
+ processedContent = processedContent.replace(/\{\{([^}]+)\}\}/g, (match, variable) => {
287
+ variable = variable.trim()
288
+ if (variable.startsWith("'") && variable.endsWith("'")) {
289
+ placeholders.push(variable)
290
+ } else {
291
+ placeholders.push(`Candy.Var(await ${variable}).html().replace(/\\n/g, "<br>")`)
292
+ }
293
+ return `%s${placeholderIndex++}`
294
+ })
295
+
296
+ processedContent = processedContent.replace(/\{!!([^}]+)!!}/g, (match, variable) => {
297
+ placeholders.push(`await ${variable.trim()}`)
298
+ return `%s${placeholderIndex++}`
299
+ })
300
+
301
+ const translationCall =
302
+ placeholders.length > 0 ? `__('${processedContent}', ${placeholders.join(', ')})` : `__('${processedContent}')`
303
+
304
+ if (attrs.raw) {
305
+ return `{!! ${translationCall} !!}`
306
+ } else {
307
+ return `{{ ${translationCall} }}`
308
+ }
309
+ } else {
310
+ return `{{ '${innerContent}' }}`
311
+ }
312
+ })
313
+ if (before === content) break
314
+ depth++
315
+ }
316
+
317
+ return content
318
+ }
319
+
320
+ async #render(file) {
321
+ let mtime = fs.statSync(file).mtimeMs
322
+ let content = fs.readFileSync(file, 'utf8')
323
+
324
+ if (this.#cache[file]?.mtime !== mtime) {
325
+ content = Form.parse(content, this.#candy)
326
+
327
+ const jsBlocks = []
328
+ content = content.replace(/<script:candy([^>]*)>([\s\S]*?)<\/script:candy>/g, (match, attrs, jsContent) => {
329
+ const placeholder = `___CANDY_JS_BLOCK_${jsBlocks.length}___`
330
+ jsBlocks.push(jsContent)
331
+ return `<script:candy${attrs}>${placeholder}</script:candy>`
332
+ })
333
+
334
+ content = this.#parseCandyTag(content)
335
+ content = content.replace(/`/g, '\\`').replace(/\$\{/g, '\\${')
336
+
337
+ jsBlocks.forEach((jsContent, index) => {
338
+ content = content.replace(`___CANDY_JS_BLOCK_${index}___`, jsContent)
339
+ })
340
+
341
+ let result = 'html += `\n' + content + '\n`'
342
+ content = content.split('\n')
343
+ for (let key in this.#functions) {
344
+ let att = ''
345
+ let func = this.#functions[key]
346
+ let matches = func.close
347
+ ? result.match(new RegExp(`${key}[\\s\\S]*?${func.close}`, 'g'))
348
+ : result.match(new RegExp(`<candy:${key}(?:\\s+[^>]*?(?:"[^"]*"|'[^']*'|[^"'>])*)?>`, 'g'))
349
+ if (!matches) continue
350
+ for (let match of matches) {
351
+ let matchForParsing = match
352
+ if (!func.close) matchForParsing = matchForParsing.replace(/^<candy:/, '').replace(/>$/, '')
353
+ const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
354
+ let attrMatch
355
+ const args = []
356
+ while ((attrMatch = attrRegex.exec(matchForParsing))) {
357
+ args.push(attrMatch[0])
358
+ }
359
+ let vars = {}
360
+ if (func.arguments)
361
+ for (let arg of args) {
362
+ const argRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/
363
+ const argMatch = argRegex.exec(arg)
364
+ if (!argMatch) continue
365
+ const argKey = argMatch[1]
366
+ const value = argMatch[3] !== undefined ? argMatch[3] : argMatch[4] !== undefined ? argMatch[4] : true
367
+ if (func.arguments[argKey] === undefined) {
368
+ att += `${argKey}="${value}"`
369
+ continue
370
+ }
371
+ vars[argKey] = value
372
+ }
373
+ if (!func.function) continue
374
+ let fun = func.function
375
+
376
+ if (key === 'for' || key === 'list') {
377
+ if (!vars.var && !vars.get) {
378
+ console.error(`"var" or "get" is required for "${match}"\n in "${file}"`)
379
+ continue
380
+ }
381
+ let constructor
382
+ if (vars.var) {
383
+ constructor = `await ${vars.var}`
384
+ delete vars.var
385
+ } else if (vars.get) {
386
+ constructor = `get('${vars.get}')`
387
+ delete vars.get
388
+ }
389
+ fun = fun.replace(/\$constructor/g, constructor)
390
+ }
391
+
392
+ for (let argKey in func.arguments) {
393
+ if (argKey === 'var' || argKey === 'get') continue
394
+ if (vars[argKey] === undefined) {
395
+ if (func.arguments[argKey] === null) console.error(`"${argKey}" is required for "${match}"\n in "${file}"`)
396
+ vars[argKey] = func.arguments[argKey]
397
+ }
398
+ fun = fun.replace(new RegExp(`\\$${argKey}`, 'g'), vars[argKey])
399
+ }
400
+ if (func.close) {
401
+ result = result.replace(match, fun + match.substring(key.length, match.length - func.close.length) + func.end)
402
+ } else {
403
+ result = result.replace(match, (func.replace ? `<${[func.replace, att].join(' ')}>` : '') + '`; ' + fun + ' html += `')
404
+ result = result.replace(
405
+ `</candy:${key}>`,
406
+ '`; ' + (func.end ?? '}') + ' html += `' + (func.replace ? `</${func.replace}>` : '')
407
+ )
408
+ }
409
+ }
410
+ }
411
+ let cache = `${nodeCrypto.createHash('md5').update(file).digest('hex')}`
412
+ if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, {recursive: true})
413
+ fs.writeFileSync(
414
+ `${CACHE_DIR}/${cache}`,
415
+ `module.exports = async (Candy, get, __) => {\nlet html = '';\n${result}\nreturn html.trim()\n}`
416
+ )
417
+ delete require.cache[require.resolve(`${__dir}/${CACHE_DIR}/${cache}`)]
418
+ if (!Candy.View) Candy.View = {}
419
+ if (!Candy.View.cache) Candy.View.cache = {}
420
+ Candy.View.cache[file] = {
421
+ mtime: mtime,
422
+ cache: cache
423
+ }
424
+ }
425
+ try {
426
+ return await require(`${__dir}/${CACHE_DIR}/${Candy.View.cache[file].cache}`)(
427
+ this.#candy,
428
+ key => this.#candy.Request.get(key),
429
+ (...args) => this.#candy.Lang.get(...args)
430
+ )
431
+ } catch (e) {
432
+ let stackLine = e.stack.split('\n')[1].match(/:(\d+):\d+/)
433
+ let line = stackLine ? parseInt(stackLine[1]) - 3 : e.lineNumber ? e.lineNumber - 3 : 'unknown'
434
+ console.error(e.toString().split('\n')[0] + `\n in line ${line}\n of "${file}"`)
435
+ }
436
+ return ''
437
+ }
438
+
439
+ // - SET PARTS
440
+ set(...args) {
441
+ if (args.length === 1 && typeof args[0] === 'object') for (let key in args[0]) this.#part[key] = args[0][key]
442
+ else if (args.length === 2) this.#part[args[0]] = args[1]
443
+
444
+ if (!this.#candy.Request.page) {
445
+ this.#candy.Request.page = this.#part.content || this.#part.all || ''
446
+ }
447
+
448
+ this.#sendEarlyHintsIfAvailable()
449
+ return this
450
+ }
451
+
452
+ skeleton(name) {
453
+ this.#part.skeleton = name
454
+ this.#sendEarlyHintsIfAvailable()
455
+ return this
456
+ }
457
+
458
+ #addNavigateAttribute(skeleton) {
459
+ skeleton = skeleton.replace(/(<[^>]+>)(\s*\{\{\s*CONTENT\s*\}\})/, (match, openTag, content) => {
460
+ if (openTag.includes('data-candy-navigate')) return match
461
+ const tagWithAttr = openTag.slice(0, -1) + ' data-candy-navigate="content">'
462
+ return tagWithAttr + content
463
+ })
464
+
465
+ const skeletonName = this.#part.skeleton || 'main'
466
+ const pageName = this.#candy.Request.page || ''
467
+
468
+ skeleton = skeleton.replace(/<html([^>]*)>/, (match, attrs) => {
469
+ const updates = []
470
+ if (!attrs.includes('data-candy-skeleton')) {
471
+ updates.push(`data-candy-skeleton="${skeletonName}"`)
472
+ }
473
+ if (!attrs.includes('data-candy-page')) {
474
+ updates.push(`data-candy-page="${pageName}"`)
475
+ }
476
+ if (updates.length === 0) return match
477
+ return `<html${attrs} ${updates.join(' ')}>`
478
+ })
479
+
480
+ return skeleton
481
+ }
482
+
483
+ #sendEarlyHintsIfAvailable() {
484
+ if (this.#candy.Request.res.headersSent) return
485
+
486
+ const routePath = this.#candy.Request.req.url.split('?')[0]
487
+ const viewPaths = []
488
+
489
+ if (this.#part.skeleton) {
490
+ viewPaths.push(`skeleton/${this.#part.skeleton}`)
491
+ }
492
+
493
+ for (let key in this.#part) {
494
+ if (['skeleton'].includes(key)) continue
495
+ if (this.#part[key]) {
496
+ const viewPath = this.#part[key].replace(/\./g, '/')
497
+ viewPaths.push(`view/${key}/${viewPath}`)
498
+ }
499
+ }
500
+
501
+ let hints = this.#earlyHints.getHints(null, routePath)
502
+
503
+ if (!hints && viewPaths.length > 0) {
504
+ hints = this.#earlyHints.getHintsForViewFiles(viewPaths)
505
+ }
506
+
507
+ if (hints && hints.length > 0) {
508
+ this.#candy.Request.setEarlyHints(hints)
509
+ }
510
+ }
511
+ }
512
+
513
+ module.exports = View