@peter.naydenov/morph 3.2.0 → 3.3.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.
package/Changelog.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
 
4
4
 
5
+ ### 3.3.0 (2025-12-13)
6
+ - [x] Use other helper from inside of helper functionsHelper functions attribute - useHelper;
7
+
8
+
9
+
5
10
  ### 3.2.0 (2025-12-01)
6
11
  - [x] Conditional actions are depricated. ( starting with `?` ). Too unconsistant with other actions. It's can be solved with normal helper functions by providing a list of possible templates. It's something that is possible even now;
7
12
  - [x] Templates can have empty placeholders - just {{ }};
package/README.md CHANGED
@@ -10,18 +10,6 @@
10
10
 
11
11
 
12
12
 
13
- ## What's new in version 3.x.x
14
-
15
- In version 3 introduced `snippets`, you can choose to render only certain placeholders or groups of placeholders from the template. This lets you use templates as collections of reusable templates, so you can extract and use just the parts you need without having to render the whole template. This makes it easier to create and manage reusable template libraries for your project or rerender only the parts that need to be updated.
16
-
17
- The `render` function now takes a command as its first argument. Available commands are: `render`, `debug`, `snippets`, `curry`, and `set`. Other arguments have no changes. Just shifted right. The second argument becomes the data, the third is dependencies, and the fourth is a list of post-processing functions.
18
-
19
- In version 3.x.x, the data is always the second argument. It can be a string, as in version 2.x.x. The term "command" is no longer used for this argument; instead, it is called "instructions". Available instructions include: `raw`, `demo`, `handshake`, `placeholders`, and `count`.
20
-
21
- How to migrate to version 3.x.x, please read the [Migration guide](./Migration.guide.md). Read more about `snippets` down below.
22
-
23
-
24
-
25
13
  ## General Information
26
14
 
27
15
  `Morph` has a logic-less template syntax. Placeholders are places surrounded by double curly braces `{{ }}` and they represents the pleces where the data will be inserted.
@@ -259,6 +247,38 @@ Helpers are templates and functions that are used by actions to decorate the dat
259
247
  - `Extended render functions` will return a string like regular render functions, but will receive a deep branch of requested data;
260
248
  - `Conditional render functions` could return null, that means: ignore this action. The result could be also a string: the name of other helper function that will render the data.
261
249
 
250
+ ### Calling Helpers within Helpers
251
+
252
+ Starting from version 3.3.0, helper functions receive a `useHelper` function in their arguments object. This allows helpers to call other helpers programmatically.
253
+
254
+ ```js
255
+ const helpers = {
256
+ // Basic helper
257
+ format: ({ data }) => `[${data}]`,
258
+
259
+ // Helper using another helper
260
+ process: ({ data, useHelper }) => {
261
+ return useHelper('format') // Uses current data
262
+ },
263
+
264
+ // Helper overriding data
265
+ customProcess: ({ data, useHelper }) => {
266
+ return useHelper('format', 'Override') // Uses provided data
267
+ },
268
+
269
+ // Helper calling a template string helper
270
+ linkToCheck: ({ data, useHelper }) => {
271
+ // 'link' is a string template helper defined elsewhere
272
+ if (data.url) return useHelper('link', { text: data.name, href: data.url })
273
+ return data.name
274
+ }
275
+ }
276
+ ```
277
+
278
+ `useHelper` signature: `useHelper(helperName, [dataOverride])`
279
+ - `helperName`: String name of the helper to call.
280
+ - `dataOverride`: Optional. Data to pass to the helper. If omitted, the current data context of the caller is used.
281
+
262
282
 
263
283
 
264
284
  ## Commands
@@ -1,4 +1,12 @@
1
1
  export default build;
2
+ export type UseHelperFn = (name: string, data?: any) => any;
3
+ export type HelperFn = (args: {
4
+ data: any;
5
+ dependencies: object;
6
+ full?: any;
7
+ useHelper?: UseHelperFn;
8
+ memory?: object;
9
+ }) => any;
2
10
  export type Template = {
3
11
  /**
4
12
  * - Data to be rendered;
@@ -7,17 +15,35 @@ export type Template = {
7
15
  /**
8
16
  * - Optional. Object with helper functions or simple templates for this template;
9
17
  */
10
- helpers?: object;
18
+ helpers?: {
19
+ [x: string]: string | HelperFn;
20
+ };
11
21
  /**
12
22
  * - Optional. Example for data to be rendered with;
13
23
  */
14
24
  handshake?: object;
15
25
  };
16
26
  export type tupleResult = any[];
27
+ /**
28
+ * @callback UseHelperFn
29
+ * @param {string} name - Name of the helper to call.
30
+ * @param {any} [data] - Optional data override.
31
+ * @returns {any} Result of the helper call.
32
+ */
33
+ /**
34
+ * @callback HelperFn
35
+ * @param {object} args
36
+ * @param {any} args.data - The data context.
37
+ * @param {object} args.dependencies - Injected dependencies.
38
+ * @param {any} [args.full] - Full data context.
39
+ * @param {UseHelperFn} [args.useHelper] - Function to call other helpers.
40
+ * @param {object} [args.memory] - internal memory state.
41
+ * @returns {any} Rendered output.
42
+ */
17
43
  /**
18
44
  * @typedef {Object} Template
19
45
  * @property {string} template - Data to be rendered;
20
- * @property {object} [helpers] - Optional. Object with helper functions or simple templates for this template;
46
+ * @property {Object.<string, HelperFn|string>} [helpers] - Optional. Object with helper functions or simple templates for this template;
21
47
  * @property {object} [handshake] - Optional. Example for data to be rendered with;
22
48
  */
23
49
  /**
@@ -1,25 +1,14 @@
1
1
  export default render;
2
2
  /**
3
- * Executes rendering and returns the rendered result.
3
+ * Renders a helper or template with the provided data and context.
4
4
  *
5
- * Handles both function-based and template-based rendering. Normalizes data objects
6
- * by converting nested objects to their 'text' properties and arrays to their first elements.
5
+ * @param {any} theData - The data to process.
6
+ * @param {string} name - The name of the helper or template to render.
7
+ * @param {object} helpers - Dictionary of available helpers.
8
+ * @param {any} original - The full original data context.
9
+ * @param {object} dependencies - injected dependencies.
10
+ * @param {...any} args - Additional arguments.
7
11
  *
8
- * @param {object|string} theData - Data to be rendered. If string, becomes the 'text' property value.
9
- * @param {string} name - Name of the render helper/template to execute
10
- * @param {object} helpers - Object containing helper functions and templates
11
- * @param {object} original - Original data context for full data access
12
- * @param {object} dependencies - External dependencies available to helpers
13
- * @param {...any} args - Additional arguments passed to the render function
14
- *
15
- * @returns {string} Rendered string result
16
- *
17
- * @example
18
- * // Render with function helper
19
- * const result = render(data, 'myHelper', helpers, originalData, deps);
20
- *
21
- * @example
22
- * // Render with template
23
- * const result = render(data, 'myTemplate', helpers, originalData, deps);
12
+ * @returns {any} The result of the rendering process.
24
13
  */
25
- declare function render(theData: object | string, name: string, helpers: object, original: object, dependencies: object, ...args: any[]): string;
14
+ declare function render(theData: any, name: string, helpers: object, original: any, dependencies: object, ...args: any[]): any;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@peter.naydenov/morph",
3
3
  "description": "Template engine",
4
- "version": "3.2.0",
4
+ "version": "3.3.0",
5
5
  "license": "MIT",
6
6
  "author": "Peter Naydenov",
7
7
  "main": "./src/main.js",
@@ -26,17 +26,23 @@ function _renderHolder ( template, data ) {
26
26
  chop = _chopTemplate (settings)( template )
27
27
  , set = settings
28
28
  ;
29
- chop.forEach ( (item,i) => {
30
- const isPlaceholder = item.includes ( set.TG_PRX )
31
- if ( isPlaceholder ) {
32
- const field = item.replace ( set.TG_PRX, '' ).replace ( set.TG_SFX, '' ).trim();
33
- if ( data.hasOwnProperty (field) && data[field] != null ) chop[i] = data[field]
34
- } // if isPlaceholder
35
- }) // forEach chop
29
+ if ( typeof chop === 'string' ) return chop
30
+ chop.forEach ( ( item, i ) => {
31
+ const isPlaceholder = item.includes ( set.TG_PRX )
32
+ if ( isPlaceholder ) {
33
+ const field = item.replace(set.TG_PRX, '').replace(set.TG_SFX, '').trim();
34
+ if (data.hasOwnProperty(field) && data[field] != null) {
35
+ let val = data [ field ]
36
+ if ( typeof val === 'object' && val.text ) val = val.text
37
+ chop[i] = val
38
+ }
39
+ } // if isPlaceholder
40
+ }) // forEach chop
36
41
  return chop.join ( '' )
37
42
  } // _renderHolder func.
38
43
 
39
44
 
45
+
40
46
  export default _renderHolder
41
47
 
42
48
 
@@ -10,10 +10,28 @@ import render from './render.js'
10
10
 
11
11
  import walk from '@peter.naydenov/walk'
12
12
 
13
+ /**
14
+ * @callback UseHelperFn
15
+ * @param {string} name - Name of the helper to call.
16
+ * @param {any} [data] - Optional data override.
17
+ * @returns {any} Result of the helper call.
18
+ */
19
+
20
+ /**
21
+ * @callback HelperFn
22
+ * @param {object} args
23
+ * @param {any} args.data - The data context.
24
+ * @param {object} args.dependencies - Injected dependencies.
25
+ * @param {any} [args.full] - Full data context.
26
+ * @param {UseHelperFn} [args.useHelper] - Function to call other helpers.
27
+ * @param {object} [args.memory] - internal memory state.
28
+ * @returns {any} Rendered output.
29
+ */
30
+
13
31
  /**
14
32
  * @typedef {Object} Template
15
33
  * @property {string} template - Data to be rendered;
16
- * @property {object} [helpers] - Optional. Object with helper functions or simple templates for this template;
34
+ * @property {Object.<string, HelperFn|string>} [helpers] - Optional. Object with helper functions or simple templates for this template;
17
35
  * @property {object} [handshake] - Optional. Example for data to be rendered with;
18
36
  */
19
37
 
@@ -40,393 +58,457 @@ function build ( tpl, extra=false, buildDependencies={} ) {
40
58
  return extra ? [ false, fail ] : fail
41
59
  }
42
60
  else { // If NO Error:
43
- const originalPlaceholders = structuredClone ( placeholders );
44
- // *** Template recognition complete. Start building the rendering function -->
45
- /**
46
- * Processes template rendering commands with provided data, dependencies, and optional post-processing functions.
47
- *
48
- * @function success
49
- * @typedef { 'render' | 'debug' | 'snippets' | 'curry' | 'set' } Command
50
- * @param {Command} [command='render'] - The command to execute. Supported commands: 'render', 'debug', 'snippets', 'curry', 'set', or 'snippets:<names>'.
51
- * @param {Object|string} [d={}] - The data object to render, or a string instruction ('raw', 'demo', 'handshake', 'placeholders', 'count').
52
- * @param {Object} [dependencies={}] - Additional dependencies to be merged with internal dependencies.
53
- * @param {...any} args - Optional post-processing functions to apply to the rendered output.
54
- * @returns {string|Array[]|function} The rendered template, snippets, data, or new function depending on the command and input.
55
- *
56
- * @throws {Error} If an unsupported command or instruction is provided.
57
- *
58
- * @example
59
- * // Render a template with data
60
- * success('render', { name: 'Alice' }, { helperFn });
61
- *
62
- * @example
63
- * // Get raw template
64
- * success('debug', 'raw');
65
- *
66
- * @example
67
- * // Get count of remaining placeholders
68
- * success('debug', 'count');
69
- *
70
- * @example
71
- * // Render only specific snippets
72
- * success('snippets:header,footer', { ... });
73
- *
74
- * @example
75
- * // Curry partial data and get new render function
76
- * const curried = success('curry', { partial: 'data' });
77
- * curried('render', { more: 'data' });
78
- *
79
- * @example
80
- * // Set placeholders and get modified template
81
- * const modified = success('set', { placeholders: { key: 'value' } });
82
- */
83
- function success ( command='render', d={}, dependencies={}, ...args ) {
84
- const cuts = structuredClone ( chop )
85
- let onlySnippets = false;
86
- if ( typeof command !== 'string' ) return `Error: Wrong command "${command}". Available commands: render, debug, snippets, set, curry.`
87
- if ( ![ 'render', 'debug', 'snippets', 'set', 'curry'].includes ( command ) && !command.startsWith('snippets') ) return `Error: Wrong command "${command}". Available commands: render, debug, snippets, set, curry.`
88
-
89
- if ( command.startsWith ( 'snippets') && command.includes ( ':' ) ) {
90
- onlySnippets = true
91
- let snippetNames = command.split ( ':' )
92
- .slice ( 1 )[0]
93
- .trim()
94
- .split ( ',' )
95
- .map ( t => t.trim() )
96
- placeholders = snippetNames.map ( item => snippets [ item ])
61
+ const originalPlaceholders = structuredClone(placeholders);
62
+ // *** Template recognition complete. Start building the rendering function -->
63
+ /**
64
+ * Processes template rendering commands with provided data, dependencies, and optional post-processing functions.
65
+ *
66
+ * @function success
67
+ * @typedef { 'render' | 'debug' | 'snippets' | 'curry' | 'set' } Command
68
+ * @param {Command} [command='render'] - The command to execute. Supported commands: 'render', 'debug', 'snippets', 'curry', 'set', or 'snippets:<names>'.
69
+ * @param {Object|string} [d={}] - The data object to render, or a string instruction ('raw', 'demo', 'handshake', 'placeholders', 'count').
70
+ * @param {Object} [dependencies={}] - Additional dependencies to be merged with internal dependencies.
71
+ * @param {...any} args - Optional post-processing functions to apply to the rendered output.
72
+ * @returns {string|Array[]|function} The rendered template, snippets, data, or new function depending on the command and input.
73
+ *
74
+ * @throws {Error} If an unsupported command or instruction is provided.
75
+ *
76
+ * @example
77
+ * // Render a template with data
78
+ * success('render', { name: 'Alice' }, { helperFn });
79
+ *
80
+ * @example
81
+ * // Get raw template
82
+ * success('debug', 'raw');
83
+ *
84
+ * @example
85
+ * // Get count of remaining placeholders
86
+ * success('debug', 'count');
87
+ *
88
+ * @example
89
+ * // Render only specific snippets
90
+ * success('snippets:header,footer', { ... });
91
+ *
92
+ * @example
93
+ * // Curry partial data and get new render function
94
+ * const curried = success('curry', { partial: 'data' });
95
+ * curried('render', { more: 'data' });
96
+ *
97
+ * @example
98
+ * // Set placeholders and get modified template
99
+ * const modified = success('set', { placeholders: { key: 'value' } });
100
+ */
101
+ function success(command = 'render', d = {}, dependencies = {}, ...args) {
102
+ const cuts = structuredClone(chop)
103
+ let onlySnippets = false;
104
+ if (typeof command !== 'string') return `Error: Wrong command "${command}". Available commands: render, debug, snippets, set, curry.`
105
+ if (!['render', 'debug', 'snippets', 'set', 'curry'].includes(command) && !command.startsWith('snippets')) return `Error: Wrong command "${command}". Available commands: render, debug, snippets, set, curry.`
106
+
107
+ if (command.startsWith('snippets') && command.includes(':')) {
108
+ onlySnippets = true
109
+ let snippetNames = command.split(':')
110
+ .slice(1)[0]
111
+ .trim()
112
+ .split(',')
113
+ .map(t => t.trim())
114
+ placeholders = snippetNames.map(item => snippets[item])
115
+ }
116
+ else if (command === 'snippets') {
117
+ onlySnippets = true
118
+ }
119
+ else if (command === 'set') {
120
+ if (typeof d !== 'object' || !d) return `Error: 'set' command requires an object with placeholders, helpers, handshake.`
121
+ // Merge helpers
122
+ const newHelpers = { ...helpers, ...(d.helpers || {}) }
123
+
124
+ // Merge handshake
125
+ const newHandshake = handshake ? { ...handshake, ...(d.handshake || {}) } : d.handshake || {}
126
+ // Modify chop for placeholders
127
+ const newChop = [...chop]
128
+ if (d.placeholders) {
129
+ Object.entries(d.placeholders).forEach(([k, v]) => {
130
+ if (!isNaN(k)) { // If k is a number
131
+ let index = placeholders[k].index;
132
+ newChop[index] = v
133
+ } // if k is a number
134
+ else { // If k is a string
135
+ let plx = placeholders.find(p => p.name === k)
136
+ newChop[plx.index] = v
137
+ }
138
+ }) // placeholders
139
+ } // if d.placeholders
140
+ const newTemplateStr = newChop.join('');
141
+ const newTpl = {
142
+ template: newTemplateStr,
143
+ helpers: newHelpers,
144
+ handshake: newHandshake
145
+ }
146
+ const result = build(newTpl, false, buildDependencies)
147
+ return typeof result === 'function' ? result : () => result
148
+ } else if (command === 'curry') {
149
+ // Render the template with current data
150
+ const rendered = success('render', d, dependencies, ...args)
151
+ // Create new template with rendered as template, keep helpers and handshake
152
+ const newTpl = {
153
+ template: rendered,
154
+ helpers,
155
+ handshake
156
+ }
157
+ const newFn = build(newTpl, false, buildDependencies)
158
+ return newFn
159
+ } else placeholders = structuredClone(originalPlaceholders) // Reset placeholders if not snippets
160
+
161
+ if (typeof d === 'string') {
162
+ switch (d) {
163
+ case 'raw':
164
+ return cuts.join('') // Original template with placeholders
165
+ case 'demo':
166
+ if (!handshake) return `Error: No handshake data.`
167
+ d = handshake // Render with handshake object
168
+ break
169
+ case 'handshake':
170
+ if (!handshake) return `Error: No handshake data.`
171
+ return structuredClone(handshake) // return a copy of handshake object
172
+ case 'helpers':
173
+ return Object.keys(helpers).join(', ')
174
+ case 'placeholders':
175
+ return placeholders.map(h => cuts[h.index]).join(', ')
176
+ case 'count':
177
+ return placeholders.length
178
+ default:
179
+ return `Error: Wrong instruction "${d}". Available instructions: raw, demo, handshake, helpers, placeholders, count.`
180
+ }
181
+ } // if d is string
182
+
183
+ const endData = [];
184
+ const memory = {};
185
+
186
+ let topLevelType = _defineDataType(d);
187
+ let deps = { ...buildDependencies, ...dependencies }
188
+ d = walk({ data: d }) // Creates copy of data to avoid mutation of the original
189
+ let original = walk({ data: d }) // Creates copy of data to avoid mutation of the original
190
+
191
+ // useHelper definition for build.js
192
+ const useHelper = (targetName, targetData) => {
193
+ // If targetData provided, user that. If not, we can't easily know "current" data here outside loop.
194
+ // But each call site passes 'theData'.
195
+ // So useHelper actually needs to be a factory or passed with current data.
196
+ // Wait, in build.js we are iterating.
197
+ // We can define a generic useHelper here that takes (targetName, targetData).
198
+ // But if targetData is missing, it should default to the caller's data.
199
+ // The caller is the helper function.
200
+ // See implementation plan: "Implementation plan: useHelper(name, override). if override missing -> current data."
201
+ // In build.js loop:
202
+ // const paramUseHelper = (tName, tData) => render(tData || theData, tName, helpers, original, deps)
203
+ // We must define this INSIDE the loop or pass 'theData' to a factory.
204
+ // It's cleaner to define a factory here if possible, but 'theData' changes.
205
+ // So we will define a createUseHelper function here.
206
+ }
207
+ const createUseHelper = (currentData) => (targetName, targetData) => {
208
+ let
209
+ targetFn = helpers[targetName]
210
+ , isCompiledTemplate = targetFn && typeof targetFn === 'function' && targetFn.length >= 2 // success(command, d) has > 1 params
211
+ ;
212
+
213
+ if (!targetFn) return `{{ Error: Helper '${targetName}' is not available}}`
214
+
215
+ if (isCompiledTemplate) {
216
+ // It's likely a compiled template (success function)
217
+ // Try calling it with 'render' command
218
+ try {
219
+ return targetFn('render', targetData || currentData, dependencies)
220
+ }
221
+ catch (e) {
222
+ // Fallback if it wasn't a compiled template but just a function with arguments
223
+ return render(targetData || currentData, targetName, helpers, original, deps, ...args)
224
+ }
225
+ }
226
+ return render(targetData || currentData, targetName, helpers, original, deps, ...args)
227
+ }
228
+ if (topLevelType === 'null') return cuts.join('')
229
+ if (topLevelType !== 'array') d = [d]
230
+
231
+ // Handle null data case - just return the template without placeholders
232
+ if (topLevelType === 'null') return cuts.join('')
233
+
234
+ d.forEach(dElement => {
235
+ placeholders.forEach(holder => { // Placeholders
236
+ const
237
+ { index, data, action } = holder // index - placeholder index, data - key of data, action - list of operations
238
+ , dataOnly = !action
239
+ , mem = structuredClone(memory)
240
+ // We cannot define extendArguments here with useHelper because we don't have 'theData' yet.
241
+ // It varies per dElement and deeper levels.
242
+ // Wait, 'info' is the data for this placeholder sort of.
243
+ // But 'extendArguments' is passed to helper.
244
+ // We need to inject 'useHelper' at the call site or update extendArguments inside the loops.
245
+ , extendArguments = { dependencies: deps, memory: mem }
246
+ ;
247
+ let info = dElement;
248
+
249
+ if (data && data.includes('/')) {
250
+ if (info.hasOwnProperty(data)) {
251
+ info = info[data]
97
252
  }
98
- else if ( command === 'snippets' ) {
99
- onlySnippets = true
253
+ else {
254
+ data.split('/').forEach(d => {
255
+ if (info.hasOwnProperty(d)) info = info[d]
256
+ else info = []
257
+ })
100
258
  }
101
- else if ( command === 'set' ) {
102
- if ( typeof d !== 'object' || !d ) return `Error: 'set' command requires an object with placeholders, helpers, handshake.`
103
- // Merge helpers
104
- const newHelpers = { ...helpers, ...(d.helpers || {}) }
105
-
106
- // Merge handshake
107
- const newHandshake = handshake ? { ...handshake, ...(d.handshake || {}) } : d.handshake || {}
108
- // Modify chop for placeholders
109
- const newChop = [...chop]
110
- if ( d.placeholders ) {
111
- Object.entries ( d.placeholders ).forEach ( ([k,v]) => {
112
- if ( !isNaN(k) ) { // If k is a number
113
- let index = placeholders[k].index;
114
- newChop[index] = v
115
- } // if k is a number
116
- else { // If k is a string
117
- let plx = placeholders.find ( p => p.name === k )
118
- newChop[ plx.index ] = v
119
- }
120
- }) // placeholders
121
- } // if d.placeholders
122
- const newTemplateStr = newChop.join ( '' );
123
- const newTpl = {
124
- template: newTemplateStr,
125
- helpers: newHelpers,
126
- handshake: newHandshake
127
- }
128
- const result = build ( newTpl, false, buildDependencies )
129
- return typeof result === 'function' ? result : () => result
130
- } else if ( command === 'curry' ) {
131
- // Render the template with current data
132
- const rendered = success('render', d, dependencies, ...args)
133
- // Create new template with rendered as template, keep helpers and handshake
134
- const newTpl = {
135
- template: rendered,
136
- helpers,
137
- handshake
138
- }
139
- const newFn = build(newTpl, false, buildDependencies)
140
- return newFn
141
- } else placeholders = structuredClone ( originalPlaceholders ) // Reset placeholders if not snippets
142
-
143
- if ( typeof d === 'string' ) {
144
- switch ( d ) {
145
- case 'raw':
146
- return cuts.join ( '' ) // Original template with placeholders
147
- case 'demo':
148
- if ( !handshake ) return `Error: No handshake data.`
149
- d = handshake // Render with handshake object
150
- break
151
- case 'handshake':
152
- if ( !handshake ) return `Error: No handshake data.`
153
- return structuredClone (handshake) // return a copy of handshake object
154
- case 'helpers' :
155
- return Object.keys ( helpers ).join ( ', ' )
156
- case 'placeholders':
157
- return placeholders.map ( h => cuts[h.index] ).join ( ', ')
158
- case 'count':
159
- return placeholders.length
160
- default:
161
- return `Error: Wrong instruction "${d}". Available instructions: raw, demo, handshake, helpers, placeholders, count.`
162
- }
163
- } // if d is string
164
-
165
- const endData = [];
166
- const memory = {};
167
-
168
- let topLevelType = _defineDataType ( d );
169
- let deps = { ...buildDependencies, ...dependencies }
170
- d = walk ({data:d}) // Creates copy of data to avoid mutation of the original
171
- let original = walk ({data:d}) // Creates copy of data to avoid mutation of the original
172
- if ( topLevelType === 'null' ) return cuts.join ( '' )
173
- if ( topLevelType !== 'array' ) d = [ d ]
174
-
175
- // Handle null data case - just return the template without placeholders
176
- if ( topLevelType === 'null' ) return cuts.join ( '' )
177
-
178
- d.forEach ( dElement => {
179
- placeholders.forEach ( holder => { // Placeholders
180
- const
181
- { index, data, action } = holder // index - placeholder index, data - key of data, action - list of operations
182
- , dataOnly = !action && data
183
- , mem = structuredClone ( memory )
184
- , extendArguments = { dependencies: deps, memory:mem }
185
- ;
186
- let info = dElement;
187
-
188
- if ( data && data.includes('/') ) {
189
- if ( info.hasOwnProperty ( data )) {
190
- info = info[data]
191
- }
192
- else {
193
- data.split('/').forEach ( d => {
194
- if ( info.hasOwnProperty(d) ) info = info[d]
195
- else info = []
196
- })
197
- }
198
- } // If data contains '/'
199
- else if ( data==='@all' || data===null || data==='@root' ) info = dElement
200
- else if ( data ) info = info[data]
201
-
202
-
203
-
204
- if ( dataOnly ) {
205
- const type = _defineDataType ( info );
206
- switch ( type ) {
207
- case 'function' :
208
- cuts[index] = info ()
209
- return
210
- case 'primitive':
211
- cuts[index] = info
212
- return
213
- case 'array':
214
- if ( _defineDataType(info[0]) === 'primitive' ) cuts[index] = info[0]
215
- return
216
- case 'object':
217
- if ( info.text ) cuts[index] = info.text
218
- return
219
- } // switch
220
- } // dataOnly
221
- else { // Data and Actions or only Actions
222
- let
223
- { dataDeepLevel, nestedData } = _defineData ( info, action )
224
- , actSetup = _actionSupply ( _setupActions ( action, dataDeepLevel ), dataDeepLevel )
225
- ;
226
-
227
- for ( let step of actSetup ) {
228
- let
229
- { type, name, level } = step
230
- , levelData = nestedData[level] || []
231
- ;
259
+ } // If data contains '/'
260
+ else if (data === '@all' || data === null || data === '@root') info = dElement
261
+ else if (data) info = info[data]
232
262
 
233
- levelData.forEach ( (theData, iData ) => {
234
-
235
- let dataType = _defineDataType ( theData )
236
-
237
- switch ( type ) { // Action type 'route','data', 'render', or mix -> different operations
238
- case 'route': // DEPRICATED in version 3.2.0. This functionality can be replaced with normal render functions
239
- switch ( dataType ) {
240
- case 'array':
241
- theData.forEach ( (d,i) => {
242
- if ( d == null ) return
243
- const dType = _defineDataType ( d )
244
- const routeName = helpers[name]( {data:d, ...extendArguments, full: d });
245
- if ( routeName == null ) return
246
- if ( dType === 'object' ) theData[i]['text'] = render ( d, routeName, helpers, original, deps )
247
- else theData[i] = render ( d, routeName, helpers, original, deps )
248
- })
249
- break
250
- case 'object':
251
- theData['text'] = render ( theData, name, helpers, original, deps )
252
- break
253
- }
254
- break
255
- case 'save' :
256
- memory[name] = structuredClone ( theData )
257
- break
258
- case 'overwrite':
259
- dElement = structuredClone ( theData )
260
- break
261
- case 'data':
262
- switch ( dataType ) {
263
- case 'array':
264
- theData.forEach ( (d,i) => theData[i] = ( d instanceof Function ) ? helpers[name]({ data:d(), ...extendArguments, full: d }) : helpers[name]( {data:d, ...extendArguments, full: d}) )
265
- break
266
- case 'object':
267
- nestedData[level] = [helpers[name]( {data:theData,...extendArguments, full: d } )]
268
- break
269
- case 'function':
270
- nestedData[level] = [helpers[name]( {data:theData(),...extendArguments, full: d } )]
271
- break
272
- case 'primitive':
273
- nestedData[level] = helpers[name]( {data:theData,...extendArguments, full: d } )
274
- break
275
- } // switch dataType
276
-
263
+
264
+
265
+ if (dataOnly) {
266
+ const type = _defineDataType(info);
267
+ switch (type) {
268
+ case 'function':
269
+ cuts[index] = info()
270
+ return
271
+ case 'primitive':
272
+ cuts[index] = info
273
+ return
274
+ case 'array':
275
+ if (_defineDataType(info[0]) === 'primitive') cuts[index] = info[0]
276
+ return
277
+ case 'object':
278
+ if (info.text) cuts[index] = info.text
279
+ return
280
+ } // switch
281
+ } // dataOnly
282
+ else { // Data and Actions or only Actions
283
+ let
284
+ { dataDeepLevel, nestedData } = _defineData(info, action)
285
+ , actSetup = _actionSupply(_setupActions(action, dataDeepLevel), dataDeepLevel)
286
+ ;
287
+
288
+ for (let step of actSetup) {
289
+ let
290
+ { type, name, level } = step
291
+ , levelData = nestedData[level] || []
292
+ ;
293
+
294
+ levelData.forEach((theData, iData) => {
295
+
296
+ let dataType = _defineDataType(theData)
297
+
298
+ switch (type) { // Action type 'route','data', 'render', or mix -> different operations
299
+ case 'route': // DEPRICATED in version 3.2.0. This functionality can be replaced with normal render functions
300
+ switch (dataType) {
301
+ case 'array':
302
+ theData.forEach((d, i) => {
303
+ if (d == null) return
304
+ const dType = _defineDataType(d)
305
+ const useHelper = createUseHelper(d);
306
+ const routeName = helpers[name]({ data: d, ...extendArguments, full: d, useHelper });
307
+ if (routeName == null) return
308
+ if (dType === 'object') theData[i]['text'] = render(d, routeName, helpers, original, deps)
309
+ else theData[i] = render(d, routeName, helpers, original, deps)
310
+ })
311
+ break
312
+ case 'object':
313
+ theData['text'] = render(theData, name, helpers, original, deps)
314
+ break
315
+ }
316
+ break
317
+ case 'save':
318
+ memory[name] = structuredClone(theData)
319
+ break
320
+ case 'overwrite':
321
+ dElement = structuredClone(theData)
322
+ break
323
+ case 'data':
324
+ switch (dataType) {
325
+ case 'array':
326
+ theData.forEach((d, i) => {
327
+ const useHelper = createUseHelper(d);
328
+ theData[i] = (d instanceof Function) ? helpers[name]({ data: d(), ...extendArguments, full: d, useHelper }) : helpers[name]({ data: d, ...extendArguments, full: d, useHelper })
329
+ })
330
+ break
331
+ case 'object':
332
+ const uhObj = createUseHelper(theData);
333
+ nestedData[level] = [helpers[name]({ data: theData, ...extendArguments, full: d, useHelper: uhObj })]
334
+ break
335
+ case 'function':
336
+ const fnData = theData();
337
+ const uhFn = createUseHelper(fnData);
338
+ nestedData[level] = [helpers[name]({ data: fnData, ...extendArguments, full: d, useHelper: uhFn })]
339
+ break
340
+ case 'primitive':
341
+ const uhPrim = createUseHelper(theData);
342
+ nestedData[level] = helpers[name]({ data: theData, ...extendArguments, full: d, useHelper: uhPrim })
343
+ break
344
+ } // switch dataType
345
+
346
+ break
347
+ case 'render':
348
+ const isRenderFunction = typeof helpers[name] === 'function'; // Render could be a function and template.
349
+ switch (dataType) {
350
+ case 'array':
351
+ if (isRenderFunction) theData.forEach((d, i) => {
352
+ if (d == null) return
353
+ const dType = _defineDataType(d);
354
+ const useHelper = createUseHelper(d);
355
+ const text = helpers[name]({ data: d, ...extendArguments, full: original, useHelper });
356
+
357
+ if (text == null) theData[i] = null
358
+ if (dType === 'object') d['text'] = text
359
+ else theData[i] = text
360
+ })
361
+ else theData.forEach((d, i) => {
362
+ if (d == null) return
363
+ const
364
+ dType = _defineDataType(d)
365
+ , text = render(d, name, helpers, original, deps)
366
+ ;
367
+ if (text == null) theData[i] = null
368
+ else if (dType === 'object') d['text'] = text
369
+ else theData[i] = text
370
+ })
371
+ break
372
+ case 'function':
373
+ const fD = theData();
374
+ const useHelperFn = createUseHelper(fD);
375
+ nestedData[level] = helpers[name]({ data: fD, ...extendArguments, full: d, useHelper: useHelperFn })
376
+ break
377
+ case 'primitive':
378
+ if (isRenderFunction) {
379
+ const uhPrim = createUseHelper(theData);
380
+ nestedData[level] = helpers[name]({ data: theData, ...extendArguments, full: d, useHelper: uhPrim })
381
+ }
382
+ else nestedData[level] = render(theData, name, helpers, original, deps)
383
+ break
384
+ case 'object':
385
+ if (isRenderFunction) {
386
+ const uhO = createUseHelper(theData);
387
+ nestedData[level][iData]['text'] = helpers[name]({ data: theData, ...extendArguments, full: d, useHelper: uhO })
388
+ }
389
+ else {
390
+ theData['text'] = render(theData, name, helpers, original, deps)
391
+ }
392
+ break
393
+ } // switch renderDataType
394
+ break;
395
+ case 'extendedRender':
396
+ // TODO: Test extendedRender
397
+ const isValid = typeof helpers[name] === 'function'; // Render could be
398
+ if (isValid) {
399
+ nestedData[0].forEach((d, i) => {
400
+ const uh = createUseHelper(d);
401
+ nestedData[0][i] = helpers[name]({ data: d, ...extendArguments, full: d, useHelper: uh })
402
+ })
403
+ }
404
+ else {
405
+ // TODO: Error...
406
+ }
407
+ break
408
+ case 'mix':
409
+ if (name === '') { // when is anonymous mixing action
410
+ switch (dataType) {
411
+ case 'object':
412
+ let kTest = Object.keys(theData).find(k => k.includes('/')); // Check if keys are breadcrumbs
413
+ if (kTest) Object.entries(theData).forEach(([k, v]) => nestedData[level][k] = v['text'])
414
+ else nestedData[level] = theData['text']
415
+ for (let i = level - 1; i >= 0; i--) {
416
+ nestedData[i] = walk({ data: nestedData[i], objectCallback: check })
417
+ }
418
+ function check({ value, breadcrumbs }) {
419
+ if (nestedData[level][breadcrumbs]) return nestedData[level][breadcrumbs]
420
+ return value
421
+ } // check
277
422
  break
278
- case 'render':
279
- const isRenderFunction = typeof helpers[name] === 'function'; // Render could be a function and template.
280
- switch ( dataType ) {
281
- case 'array':
282
- if ( isRenderFunction ) theData.forEach ( (d,i) => {
283
- if ( d == null ) return
284
- const dType = _defineDataType ( d );
285
- const text = helpers[name]( {data:d, ...extendArguments, full: original });
286
-
287
- if ( text == null ) theData[i] = null
288
- if ( dType === 'object' ) d['text'] = text
289
- else theData[i] = text
290
- })
291
- else theData.forEach ( (d,i) => {
292
- if ( d == null ) return
293
- const
294
- dType = _defineDataType ( d )
295
- , text = render ( d, name, helpers, original, deps )
296
- ;
297
- if ( text == null ) theData[i] = null
298
- else if ( dType === 'object' ) d['text'] = text
299
- else theData[i] = text
300
- })
301
- break
302
- case 'function':
303
- nestedData[level] = helpers[name]( {data:theData(), ...extendArguments, full: d} )
304
- break
305
- case 'primitive':
306
- if ( isRenderFunction ) nestedData[level] = helpers[name]({ data:theData, ...extendArguments, full: d} )
307
- else nestedData[level] = render ( theData, name, helpers, original, deps )
308
- break
309
- case 'object':
310
- if ( isRenderFunction ) nestedData[level][iData]['text'] = helpers[name]({ data:theData, ...extendArguments, full: d })
311
- else {
312
- theData [ 'text' ] = render ( theData, name, helpers, original, deps )
313
- }
314
- break
315
- } // switch renderDataType
316
- break;
317
- case 'extendedRender':
318
- // TODO: Test extendedRender
319
- const isValid = typeof helpers[name] === 'function'; // Render could be a function and template.
320
- if ( isValid ) {
321
- nestedData[0].forEach ( (d,i) => nestedData[0][i] = helpers[name]({ data:d, ...extendArguments, full: d }) )
423
+ case 'array':
424
+ theData.forEach((x, i) => {
425
+ if (i > 0) {
426
+ let xType = _defineDataType(x);
427
+ if (x == null) return
428
+ if (xType === 'object') theData[0] += `${x.text}`
429
+ else theData[0] += `${x}`
430
+ theData.toSpliced(i, 1)
322
431
  }
323
- else {
324
- // TODO: Error...
432
+ else {
433
+ let xxType = _defineDataType(x);
434
+ theData[0] = ''
435
+ if (xxType === null) return
436
+ else if (xxType === 'object') theData[0] = `${x.text}`
437
+ else theData[0] = `${x}`
325
438
  }
439
+ })
440
+ theData.length = 1
326
441
  break
327
- case 'mix':
328
- if ( name === '' ) { // when is anonymous mixing action
329
- switch ( dataType ) {
330
- case 'object':
331
- let kTest = Object.keys ( theData ).find ( k => k.includes ( '/' ) ); // Check if keys are breadcrumbs
332
- if ( kTest ) Object.entries( theData ).forEach( ([k,v]) => nestedData[level][k] = v['text'] )
333
- else nestedData[level] = theData['text']
334
- for ( let i=level-1; i >= 0; i-- ) {
335
- nestedData[i] = walk ({data:nestedData[i], objectCallback:check})
336
- }
337
- function check ({ value, breadcrumbs }) {
338
- if ( nestedData[level][breadcrumbs] ) return nestedData[level][breadcrumbs]
339
- return value
340
- } // check
341
- break
342
- case 'array':
343
- theData.forEach ( (x,i) => {
344
- if ( i > 0 ) {
345
- let xType = _defineDataType ( x );
346
- if ( x == null ) return
347
- if ( xType === 'object' ) theData[0] += `${x.text}`
348
- else theData[0] += `${x}`
349
- theData.toSpliced(i,1)
350
- }
351
- else {
352
- let xxType = _defineDataType ( x );
353
- theData[0] = ''
354
- if ( xxType === null ) return
355
- else if ( xxType === 'object' ) theData[0] = `${x.text}`
356
- else theData[0] = `${x}`
357
- }
358
- })
359
- theData.length = 1
360
- break
361
- } // switch dataType
362
- } // if name === ''
363
- else {
364
- let
365
- val = helpers[name]({ data:theData, ...extendArguments, full: d })
366
- , valType = _defineDataType ( val )
367
- ;
368
-
369
- theData.forEach ( ( x, i ) => theData.splice ( i, 1) )
370
- theData.length = 0
371
- switch ( valType ) {
372
- case 'primitive':
373
- theData[0] = val
374
- break
375
- case 'array':
376
- theData.push ( ...val )
377
- break
378
- } // switch valType
379
- }
380
- break
381
- default:
382
- break
383
- }
384
- }) // levelData
385
- } // for step of actSetup
386
-
387
- if ( nestedData instanceof Array &&
388
- nestedData.length === 1 &&
389
- nestedData[0] instanceof Array
390
- ) nestedData = nestedData[0]
391
- if ( nestedData[0] == null ) return
392
-
393
- let
394
- accType = _defineDataType ( nestedData[0] )
395
- , fineData = nestedData[0]
396
- ;
442
+ } // switch dataType
443
+ } // if name === ''
444
+ else {
445
+ const dType = _defineDataType(d);
446
+ const useHelper = createUseHelper(d);
447
+ let val = helpers[name]({ data: theData, ...extendArguments, full: d, useHelper });
448
+ let valType = _defineDataType(val)
449
+ ;
397
450
 
398
- switch ( accType ) {
451
+ theData.forEach((x, i) => theData.splice(i, 1))
452
+ theData.length = 0
453
+ switch (valType) {
399
454
  case 'primitive':
400
- if ( fineData == null ) return
401
- cuts[index] = fineData
402
- break
403
- case 'object':
404
- if (fineData['text'] == null ) return
405
- cuts[index] = fineData['text']
455
+ theData[0] = val
406
456
  break
407
457
  case 'array':
408
- const aType = _defineDataType ( fineData[0] )
409
- if ( aType === 'object' ) cuts[index] = fineData.map ( x => x.text ).join ( '')
410
- else cuts[index] = fineData.join ( '' )
458
+ theData.push(...val)
411
459
  break
412
- } // switch accType
413
- } // else other
414
- }) // forEach placeholders
415
-
416
- if ( onlySnippets ) endData.push ( placeholders.map ( x => cuts[x.index] ).join ( '<~>' ) )
417
- else endData.push ( cuts.join ( '' ))
418
- }) // forEach d
419
-
420
- if ( topLevelType === 'array' ) return endData
421
- // Execute postprocess functions
422
- if (args) return args.reduce ( (acc, fn) => {
423
- if ( typeof fn !== 'function' ) return acc
424
- return fn ( acc, deps )
425
- }, endData.join ( '' ) )
426
- else return endData.join ( '' )
427
- } // success func.
428
- return extra ? [ true, success ] : success
429
- } // if no error
460
+ } // switch valType
461
+ }
462
+ break
463
+ default:
464
+ break
465
+ }
466
+ }) // levelData
467
+ } // for step of actSetup
468
+
469
+ if (nestedData instanceof Array &&
470
+ nestedData.length === 1 &&
471
+ nestedData[0] instanceof Array
472
+ ) nestedData = nestedData[0]
473
+ if (nestedData[0] == null) return
474
+
475
+ let
476
+ accType = _defineDataType(nestedData[0])
477
+ , fineData = nestedData[0]
478
+ ;
479
+
480
+ switch (accType) {
481
+ case 'primitive':
482
+ if (fineData == null) return
483
+ cuts[index] = fineData
484
+ break
485
+ case 'object':
486
+ if (fineData['text'] == null) return
487
+ cuts[index] = fineData['text']
488
+ break
489
+ case 'array':
490
+ const aType = _defineDataType(fineData[0])
491
+ if (aType === 'object') cuts[index] = fineData.map(x => x.text).join('')
492
+ else cuts[index] = fineData.join('')
493
+ break
494
+ } // switch accType
495
+ } // else other
496
+ }) // forEach placeholders
497
+
498
+ if (onlySnippets) endData.push(placeholders.map(x => cuts[x.index]).join('<~>'))
499
+ else endData.push(cuts.join(''))
500
+ }) // forEach d
501
+
502
+ if (topLevelType === 'array') return endData
503
+ // Execute postprocess functions
504
+ if (args) return args.reduce((acc, fn) => {
505
+ if (typeof fn !== 'function') return acc
506
+ return fn(acc, deps)
507
+ }, endData.join(''))
508
+ else return endData.join('')
509
+ } // success func.
510
+ return extra ? [true, success] : success
511
+ } // if no error
430
512
  } // build func.
431
513
 
432
514
 
@@ -1,60 +1,34 @@
1
- import _renderHolder from "./_renderHolder.js"
1
+
2
+ import _renderHolder from './_renderHolder.js'
2
3
 
3
4
 
4
5
 
5
6
  /**
6
- * Executes rendering and returns the rendered result.
7
- *
8
- * Handles both function-based and template-based rendering. Normalizes data objects
9
- * by converting nested objects to their 'text' properties and arrays to their first elements.
10
- *
11
- * @param {object|string} theData - Data to be rendered. If string, becomes the 'text' property value.
12
- * @param {string} name - Name of the render helper/template to execute
13
- * @param {object} helpers - Object containing helper functions and templates
14
- * @param {object} original - Original data context for full data access
15
- * @param {object} dependencies - External dependencies available to helpers
16
- * @param {...any} args - Additional arguments passed to the render function
17
- *
18
- * @returns {string} Rendered string result
7
+ * Renders a helper or template with the provided data and context.
8
+ *
9
+ * @param {any} theData - The data to process.
10
+ * @param {string} name - The name of the helper or template to render.
11
+ * @param {object} helpers - Dictionary of available helpers.
12
+ * @param {any} original - The full original data context.
13
+ * @param {object} dependencies - injected dependencies.
14
+ * @param {...any} args - Additional arguments.
19
15
  *
20
- * @example
21
- * // Render with function helper
22
- * const result = render(data, 'myHelper', helpers, originalData, deps);
23
- *
24
- * @example
25
- * // Render with template
26
- * const result = render(data, 'myTemplate', helpers, originalData, deps);
16
+ * @returns {any} The result of the rendering process.
27
17
  */
28
- function render ( theData, name, helpers, original, dependencies, ...args ) {
29
- // *** Executes rendering and return the results
30
- if ( theData instanceof Object ) { // Make sure all properties are not objects
31
- Object.entries ( theData ).forEach ( ([key, value]) => {
32
- if ( value instanceof Object ) theData[key] = value['text']
33
- if ( value instanceof Array ) theData[key] = value[0]
34
- })
35
- }
36
-
37
- /**
38
- * Normalizes render data by wrapping strings in text objects.
39
- *
40
- * @param {any} d - Data to normalize
41
- * @returns {object} Returns { text: d } if d is string, otherwise returns d unchanged
42
- */
43
- function setRenderData ( d={} ) {
44
- if ( typeof d === 'string' ) return { text: d }
45
- else return d
46
- } // setRenderData func.
47
-
48
- if (!helpers[name]) return `{{ Error: Helper '${name}' is not available}}`
49
- const isRenderFunction = typeof helpers[name] === 'function'; // Render could be a function or template.
50
- theData = setRenderData ( theData )
51
-
52
- if ( isRenderFunction ) return helpers[name]( { theData, dependencies, full:original}, ...args )
53
- else return _renderHolder ( helpers[name], theData )
18
+ function render ( theData, name, helpers, original, dependencies, ...args) {
19
+ const useHelper = ( targetName, targetData ) => render ( targetData || theData, targetName, helpers, original, dependencies, ...args )
20
+ if (!helpers[name]) return `( Error: Helper '${name}' is not available )`
21
+
22
+ const isRenderFunction = typeof helpers[name] === 'function';
23
+
24
+ if ( isRenderFunction ) return helpers[name]({ data: theData, dependencies, full: original, useHelper }, ...args)
25
+ else {
26
+ let dataForHolder = theData
27
+ if ( typeof theData !== 'object' || theData === null ) dataForHolder = { text: theData }
28
+ return _renderHolder ( helpers[name], dataForHolder )
29
+ }
54
30
  } // render func.
55
31
 
56
32
 
57
33
 
58
34
  export default render
59
-
60
-