@peter.naydenov/morph 3.2.0 → 3.3.1

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.
@@ -1,19 +1,36 @@
1
- import _chopTemplate from "./_chopTemplates.js"
2
- import _defineData from "./_defineData.js"
3
- import _actionSupply from "./_actionSupply.js"
4
- import _setupActions from "./_setupActions.js"
5
- import _readTemplate from "./_readTemplate.js"
6
- import _renderHolder from './_renderHolder.js'
7
- import _defineDataType from "./_defineType.js"
8
- import render from './render.js'
1
+ import _chopTemplate from "./_chopTemplates.js"
2
+ import _readTemplate from "./_readTemplate.js"
3
+ import _defineDataType from "./_defineType.js"
4
+ import walk from '@peter.naydenov/walk'
5
+ import processPlaceholders from './processPlaceholders.js'
6
+ import {
7
+ handleDebug
8
+ , handleSet
9
+ , handleSnippets } from './processCommands.js'
9
10
 
10
11
 
11
- import walk from '@peter.naydenov/walk'
12
+ /**
13
+ * @callback UseHelperFn
14
+ * @param {string} name - Name of the helper to call.
15
+ * @param {any} [data] - Optional data override.
16
+ * @returns {any} Result of the helper call.
17
+ */
18
+
19
+ /**
20
+ * @callback HelperFn
21
+ * @param {object} args
22
+ * @param {any} args.data - The data context.
23
+ * @param {object} args.dependencies - Injected dependencies.
24
+ * @param {any} [args.full] - Full data context.
25
+ * @param {UseHelperFn} [args.useHelper] - Function to call other helpers.
26
+ * @param {object} [args.memory] - internal memory state.
27
+ * @returns {any} Rendered output.
28
+ */
12
29
 
13
30
  /**
14
31
  * @typedef {Object} Template
15
32
  * @property {string} template - Data to be rendered;
16
- * @property {object} [helpers] - Optional. Object with helper functions or simple templates for this template;
33
+ * @property {Object.<string, HelperFn|string>} [helpers] - Optional. Object with helper functions or simple templates for this template;
17
34
  * @property {object} [handshake] - Optional. Example for data to be rendered with;
18
35
  */
19
36
 
@@ -32,401 +49,103 @@ import walk from '@peter.naydenov/walk'
32
49
  * @param {object} [buildDependencies] - Optional. External dependencies injected;
33
50
  * @returns {function|tupleResult} - rendering function
34
51
  */
35
- function build ( tpl, extra=false, buildDependencies={} ) {
36
- let { hasError, placeholders, chop, helpers, handshake, snippets } = _readTemplate ( tpl );
37
-
38
- if ( hasError ) {
39
- function fail () { return hasError }
40
- return extra ? [ false, fail ] : fail
41
- }
42
- 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 ])
97
- }
98
- else if ( command === 'snippets' ) {
99
- onlySnippets = true
100
- }
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 = {};
52
+ function build ( tpl, extra = false, buildDependencies = {}) {
53
+ let { hasError, placeholders, chop, helpers, handshake, snippets } = _readTemplate(tpl);
167
54
 
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
- ;
232
-
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
-
277
- 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 }) )
322
- }
323
- else {
324
- // TODO: Error...
325
- }
326
- 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
- ;
397
-
398
- switch ( accType ) {
399
- 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']
406
- break
407
- 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 ( '' )
411
- 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
55
+ if ( hasError ) {
56
+ function fail() { return hasError }
57
+ return extra ? [false, fail] : fail
58
+ }
59
+ else {
60
+ const originalPlaceholders = structuredClone(placeholders);
61
+
62
+ function success ( command = 'render', d = {}, dependencies = {}, ...args ) {
63
+ const cuts = structuredClone(chop)
64
+ let onlySnippets = false;
65
+
66
+ // Command Validation
67
+ if (typeof command !== 'string') return `Error: Wrong command "${command}". Available commands: render, debug, snippets, set, curry.`
68
+ if (!['render', 'debug', 'snippets', 'set', 'curry'].includes(command) && !command.startsWith('snippets')) return `Error: Wrong command "${command}". Available commands: render, debug, snippets, set, curry.`
69
+
70
+
71
+ // Handle Commands
72
+ if (command.startsWith('snippets')) {
73
+ onlySnippets = true
74
+ const subset = handleSnippets(command, snippets)
75
+ if (subset) placeholders = subset
76
+ }
77
+ else if (command === 'snippets') {
78
+ onlySnippets = true
79
+ }
80
+ else if (command === 'set') {
81
+ return handleSet(d, { helpers, handshake, placeholders, chop, build, buildDependencies })
82
+ }
83
+ else if (command === 'curry') {
84
+ // Curry logic
85
+ const rendered = success ( 'render', d, dependencies, ...args );
86
+ const newTpl = {
87
+ template: rendered,
88
+ helpers,
89
+ handshake
90
+ };
91
+ return build ( newTpl, false, buildDependencies )
92
+ }
93
+ else {
94
+ placeholders = structuredClone ( originalPlaceholders )
95
+ }
96
+
97
+
98
+ // Handle 'DEBUG' (if d is string and command render/debug?)
99
+ // Original logic: if (typeof d === 'string').
100
+ // Wait, success('debug', 'raw') -> command='debug', d='raw'.
101
+ // success('render', 'demo') -> command='render', d='demo'.
102
+ // So any string d is treated as instruction ??
103
+ if (typeof d === 'string') {
104
+ const res = handleDebug(d, { handshake, helpers, placeholders, cuts })
105
+ // If handleDebug returns string/number/object, return it.
106
+ // Unless d='demo' which returns handshake object, then we proceed to render?
107
+ if (d === 'demo' && typeof res === 'object') d = res
108
+ else return res
109
+ }
110
+ // Proceed to Rendering
111
+ const memory = {};
112
+ let topLevelType = _defineDataType(d);
113
+ let deps = { ...buildDependencies, ...dependencies }
114
+
115
+ d = walk({ data: d })
116
+ let original = walk({ data: d })
117
+
118
+ if (topLevelType === 'null') return cuts.join('')
119
+ if (topLevelType !== 'array') d = [d]
120
+ if (topLevelType === 'null') return cuts.join('') // Redundant check?
121
+
122
+
123
+ const endData = processPlaceholders({
124
+ d
125
+ , chop
126
+ , placeholders
127
+ , original
128
+ , helpers
129
+ , dependencies: deps
130
+ , memory
131
+ , args
132
+ , onlySnippets
133
+ });
134
+
135
+
136
+ if ( topLevelType === 'array' ) return endData
137
+
138
+ // Post-process
139
+ if (args) {
140
+ return args.reduce ( ( acc, fn ) => {
141
+ if (typeof fn !== 'function') return acc
142
+ return fn ( acc, deps )
143
+ }, endData.join(''))
144
+ }
145
+ else return endData.join ( '' )
146
+ } // success func.
147
+ return extra ? [true, success] : success
148
+ }
430
149
  } // build func.
431
150
 
432
151