@peter.naydenov/morph 0.0.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.
- package/Changelog.md +7 -0
- package/LICENSE +21 -0
- package/Migration.guide.md +3 -0
- package/README.md +207 -0
- package/dist/morph.cjs +1 -0
- package/dist/morph.esm.mjs +1 -0
- package/dist/morph.umd.js +1 -0
- package/package.json +61 -0
- package/rollup.config.js +42 -0
- package/src/main.js +100 -0
- package/src/methods/_actionSupply.js +19 -0
- package/src/methods/_chopTemplates.js +60 -0
- package/src/methods/_defineData.js +32 -0
- package/src/methods/_defineType.js +16 -0
- package/src/methods/_readTemplate.js +78 -0
- package/src/methods/_renderHolder.js +26 -0
- package/src/methods/_setupActions.js +69 -0
- package/src/methods/build.js +239 -0
- package/src/methods/render.js +26 -0
- package/src/methods/settings.js +6 -0
- package/test/01-chop.test.js +24 -0
- package/test/02-build.test.js +470 -0
- package/test/03-storage.test.js +78 -0
- package/test/04-commands.js +73 -0
- package/test/05-data.test.js +168 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import settings from "./settings.js";
|
|
2
|
+
import _chopTemplate from "./_chopTemplates.js"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
function readData ( field ) {
|
|
7
|
+
return field ? field : null
|
|
8
|
+
} // readData func.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function _readTemplate ( tpl ) {
|
|
13
|
+
const
|
|
14
|
+
{ template, helpers, handshake } = tpl
|
|
15
|
+
,{ TG_PRX, TG_SFX, TG_SIZE_P, TG_SIZE_S } = settings
|
|
16
|
+
, placeholders = []
|
|
17
|
+
;
|
|
18
|
+
|
|
19
|
+
let hasError = null;
|
|
20
|
+
const chop = _chopTemplate (settings)( template );
|
|
21
|
+
|
|
22
|
+
if ( typeof chop === 'string' ) hasError = chop
|
|
23
|
+
else {
|
|
24
|
+
chop.forEach ( (item,i) => {
|
|
25
|
+
const
|
|
26
|
+
// Placeholder contains: Opening tag(TG_PRX), dataName, delimiter(:), list of operations, closing tag(TG_SFX)
|
|
27
|
+
finding = RegExp ( TG_PRX + '\\s*(.*?)\\s*(?::\\s*(.*?)\\s*)?' + TG_SFX, 'g' )
|
|
28
|
+
, isPlaceholder = item.includes( TG_PRX )
|
|
29
|
+
;
|
|
30
|
+
|
|
31
|
+
if ( isPlaceholder ) {
|
|
32
|
+
const x = finding.exec ( item )
|
|
33
|
+
|
|
34
|
+
placeholders.push ( {
|
|
35
|
+
index: i
|
|
36
|
+
, data : readData ( x[1] )
|
|
37
|
+
, action : x[2] ? x[2].split(',').map ( x => x.trim()) : null
|
|
38
|
+
})
|
|
39
|
+
} // if isPlaceholder
|
|
40
|
+
}) // forEach chop
|
|
41
|
+
} // else error
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// Check helpers - sanity check
|
|
46
|
+
placeholders.forEach ( holder => {
|
|
47
|
+
if ( !holder.action ) return
|
|
48
|
+
holder.action.every ( act => {
|
|
49
|
+
if ( act === '#' ) return true
|
|
50
|
+
if ( act.startsWith ( '?' )) act = act.replace ( '?', '' )
|
|
51
|
+
if ( act.startsWith ( '+' )) act = act.replace ( '+', '' )
|
|
52
|
+
if ( act.startsWith ( '[]' )) act = act.replace ( '[]', '' )
|
|
53
|
+
if ( act.startsWith ( '>' )) act = act.replace ( '>', '' )
|
|
54
|
+
if ( act === '' ) return true
|
|
55
|
+
if ( helpers[act] ) return true
|
|
56
|
+
else {
|
|
57
|
+
hasError = `Error: Missing helper: ${act}`
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
}) // every action
|
|
61
|
+
}) // forEach placeholders
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
hasError
|
|
67
|
+
, placeholders
|
|
68
|
+
, chop
|
|
69
|
+
, helpers
|
|
70
|
+
, handshake
|
|
71
|
+
}
|
|
72
|
+
} // _readTemplate func.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export default _readTemplate
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import _chopTemplate from './_chopTemplates.js'
|
|
2
|
+
import settings from './settings.js'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
function _renderHolder ( template, data ) {
|
|
7
|
+
// Data should be an object. No array, no string.
|
|
8
|
+
if ( data == null ) return null
|
|
9
|
+
const
|
|
10
|
+
chop = _chopTemplate (settings)( template )
|
|
11
|
+
, set = settings
|
|
12
|
+
;
|
|
13
|
+
chop.forEach ( (item,i) => {
|
|
14
|
+
const isPlaceholder = item.includes ( set.TG_PRX )
|
|
15
|
+
if ( isPlaceholder ) {
|
|
16
|
+
const field = item.replace ( set.TG_PRX, '' ).replace ( set.TG_SFX, '' ).trim();
|
|
17
|
+
if ( data[field] ) chop[i] = data[field]
|
|
18
|
+
} // if isPlaceholder
|
|
19
|
+
}) // forEach chop
|
|
20
|
+
return chop.join ( '' )
|
|
21
|
+
} // _renderHolder func.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export default _renderHolder
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function _setupActions ( actions, dataDeepLevel=10 ) {
|
|
2
|
+
let
|
|
3
|
+
actSetup = {}
|
|
4
|
+
, actLevel = 0
|
|
5
|
+
, actionList = [...actions]
|
|
6
|
+
, i = 0
|
|
7
|
+
;
|
|
8
|
+
|
|
9
|
+
do {
|
|
10
|
+
actSetup[i] = []
|
|
11
|
+
i++
|
|
12
|
+
} while ( i <= dataDeepLevel )
|
|
13
|
+
|
|
14
|
+
actionList.every ( act => {
|
|
15
|
+
if ( act === '#' ) { // it's a change level action
|
|
16
|
+
actLevel++
|
|
17
|
+
if ( actLevel > dataDeepLevel ) return false
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
if ( act.startsWith ( '?' ) ) { // it's a condition render action
|
|
21
|
+
actSetup[actLevel].push ({
|
|
22
|
+
type: 'route'
|
|
23
|
+
, name: act.replace ( '?', '' )
|
|
24
|
+
, level: actLevel
|
|
25
|
+
})
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
if ( act.startsWith ( '+' ) ) { // it's a extended render action
|
|
29
|
+
actSetup[actLevel].push ({
|
|
30
|
+
type: 'extendedRender'
|
|
31
|
+
, name: act.replace ( '+', '' )
|
|
32
|
+
, level: actLevel
|
|
33
|
+
})
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
if ( act.startsWith ( '[]' )) { // it's a mixing action
|
|
37
|
+
actSetup[actLevel].push ({
|
|
38
|
+
type: 'mix'
|
|
39
|
+
, name: act.replace ( '[]', '' )
|
|
40
|
+
, level: actLevel
|
|
41
|
+
})
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
if ( act.startsWith ( '>' ) ) { // it's a data action. Result will be merged to existing data
|
|
45
|
+
actSetup[actLevel].push ({
|
|
46
|
+
type: 'data'
|
|
47
|
+
, name: act.replace ( '>', '' )
|
|
48
|
+
, level: actLevel
|
|
49
|
+
})
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
if ( act === '' ) {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
actSetup[actLevel].push ({
|
|
56
|
+
type: 'render'
|
|
57
|
+
, name: act
|
|
58
|
+
, level: actLevel
|
|
59
|
+
})
|
|
60
|
+
return true
|
|
61
|
+
}) // actionList every
|
|
62
|
+
return actSetup
|
|
63
|
+
} // _setupActions func.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export default _setupActions
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,239 @@
|
|
|
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'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import walk from '@peter.naydenov/walk'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function build ( tpl ) {
|
|
16
|
+
const { hasError, placeholders, chop, helpers, handshake } = _readTemplate ( tpl );
|
|
17
|
+
if ( hasError ) {
|
|
18
|
+
return function fail () {
|
|
19
|
+
return hasError
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else { // If no errors:
|
|
23
|
+
let cuts = structuredClone ( chop );
|
|
24
|
+
// *** Template recognition complete. Start building the rendering function -->
|
|
25
|
+
|
|
26
|
+
return function success ( d={}, ...args ) {
|
|
27
|
+
const
|
|
28
|
+
topLevelType = _defineDataType ( d )
|
|
29
|
+
, endData = []
|
|
30
|
+
;
|
|
31
|
+
if ( topLevelType === 'null' ) return cuts.join ( '' )
|
|
32
|
+
// Commands : raw, demo, handshake, placeholders
|
|
33
|
+
if ( typeof d === 'string' ) { // 'd' is a string when we want to debug the template
|
|
34
|
+
switch ( d ) {
|
|
35
|
+
case 'raw':
|
|
36
|
+
return cuts.join('') // Original template with placeholders
|
|
37
|
+
case 'demo':
|
|
38
|
+
if ( !handshake ) return `Error: No handshake data.`
|
|
39
|
+
d = handshake // Render with handshake object
|
|
40
|
+
break
|
|
41
|
+
case 'handshake':
|
|
42
|
+
if ( !handshake ) return `Error: No handshake data.`
|
|
43
|
+
return structuredClone (handshake) // return a copy of handshake object
|
|
44
|
+
case 'placeholders':
|
|
45
|
+
return placeholders.map ( h => cuts[h.index] ).join ( ', ')
|
|
46
|
+
default:
|
|
47
|
+
return `Error: Wrong command "${d}". Available commands: raw, demo, handshake, placeholders.`
|
|
48
|
+
}
|
|
49
|
+
} // if 'd' is string
|
|
50
|
+
|
|
51
|
+
if ( topLevelType !== 'array' ) d = [ d ]
|
|
52
|
+
|
|
53
|
+
// TODO: If 'd' is null -> then no data for all the placeholders
|
|
54
|
+
d.forEach ( d => {
|
|
55
|
+
placeholders.forEach ( holder => { // Placeholders
|
|
56
|
+
const
|
|
57
|
+
{ index, data, action } = holder
|
|
58
|
+
, dataOnly = !action && data
|
|
59
|
+
;
|
|
60
|
+
|
|
61
|
+
if ( dataOnly ) {
|
|
62
|
+
const
|
|
63
|
+
info = d[data]
|
|
64
|
+
, type = _defineDataType ( info )
|
|
65
|
+
;
|
|
66
|
+
switch ( type ) {
|
|
67
|
+
case 'function' :
|
|
68
|
+
cuts[index] = info ()
|
|
69
|
+
return
|
|
70
|
+
case 'primitive':
|
|
71
|
+
cuts[index] = info
|
|
72
|
+
return
|
|
73
|
+
case 'array':
|
|
74
|
+
if ( _defineDataType(info[0]) === 'primitive' ) cuts[index] = info[0]
|
|
75
|
+
return
|
|
76
|
+
case 'object':
|
|
77
|
+
if ( info.text ) cuts[index] = info.text
|
|
78
|
+
return
|
|
79
|
+
} // switch
|
|
80
|
+
} // dataOnly
|
|
81
|
+
else { // Data and Actions
|
|
82
|
+
const
|
|
83
|
+
{ dataDeepLevel, nestedData } = (data==='@all' || data===null || data==='@root' ) ? _defineData ( d ) : _defineData ( d[data] )
|
|
84
|
+
, actSetup = _actionSupply ( _setupActions ( action, dataDeepLevel ), dataDeepLevel )
|
|
85
|
+
;
|
|
86
|
+
|
|
87
|
+
for ( let step of actSetup ) {
|
|
88
|
+
let { type, name, level } = step
|
|
89
|
+
let
|
|
90
|
+
theData = nestedData[level]
|
|
91
|
+
, dataType = _defineDataType ( theData )
|
|
92
|
+
;
|
|
93
|
+
switch ( type ) { // Action type 'route','data', 'render', or mix -> different operations
|
|
94
|
+
case 'route':
|
|
95
|
+
switch ( dataType ) {
|
|
96
|
+
case 'array':
|
|
97
|
+
theData.forEach ( (d,i) => {
|
|
98
|
+
if ( d == null ) return
|
|
99
|
+
const dType = _defineDataType ( d )
|
|
100
|
+
const routeName = helpers[name]( d );
|
|
101
|
+
if ( routeName == null ) return
|
|
102
|
+
if ( dType === 'object' ) theData[i]['text'] = render ( d, routeName, helpers )
|
|
103
|
+
else theData[i] = render ( d, routeName, helpers )
|
|
104
|
+
})
|
|
105
|
+
break
|
|
106
|
+
case 'object':
|
|
107
|
+
theData['text'] = render ( theData, routeName, helpers )
|
|
108
|
+
break
|
|
109
|
+
case 'primitive':
|
|
110
|
+
nestedData[level] = render ( theData, routeName, helpers )
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
break
|
|
114
|
+
case 'data':
|
|
115
|
+
switch ( dataType ) {
|
|
116
|
+
case 'array':
|
|
117
|
+
theData.forEach ( (d,i) => theData[i] = ( d instanceof Function ) ? helpers[name]( d() ) : helpers[name]( d ) )
|
|
118
|
+
break
|
|
119
|
+
case 'object':
|
|
120
|
+
nestedData[level] = helpers[name]( theData )
|
|
121
|
+
break
|
|
122
|
+
case 'function':
|
|
123
|
+
nestedData[level] = helpers[name]( theData() )
|
|
124
|
+
break
|
|
125
|
+
case 'primitive':
|
|
126
|
+
nestedData[level] = helpers[name]( theData )
|
|
127
|
+
break
|
|
128
|
+
} // switch dataType
|
|
129
|
+
|
|
130
|
+
break
|
|
131
|
+
case 'render':
|
|
132
|
+
const isRenderFunction = typeof helpers[name] === 'function'; // Render could be a function and template.
|
|
133
|
+
switch ( dataType ) {
|
|
134
|
+
case 'array':
|
|
135
|
+
if ( isRenderFunction ) theData.forEach ( (d,i) => {
|
|
136
|
+
if ( d == null ) return
|
|
137
|
+
const dType = _defineDataType ( d );
|
|
138
|
+
const text = helpers[name]( d );
|
|
139
|
+
if ( text == null ) theData[i] = null
|
|
140
|
+
if ( dType === 'object' ) d['text'] = text
|
|
141
|
+
else theData[i] = text
|
|
142
|
+
})
|
|
143
|
+
else theData.forEach ( (d,i) => {
|
|
144
|
+
if ( d == null ) return
|
|
145
|
+
const
|
|
146
|
+
dType = _defineDataType ( d )
|
|
147
|
+
, text = render ( d, name, helpers )
|
|
148
|
+
;
|
|
149
|
+
if ( text == null ) theData[i] = null
|
|
150
|
+
if ( dType === 'object' ) d['text'] = text
|
|
151
|
+
else theData[i] = text
|
|
152
|
+
})
|
|
153
|
+
break
|
|
154
|
+
case 'primitive':
|
|
155
|
+
nestedData[level] = render ( theData, name, helpers )
|
|
156
|
+
break
|
|
157
|
+
case 'object':
|
|
158
|
+
let kTest = Object.keys ( theData ).find ( k => k.includes ( '/' ) ); // Check if keys are breadcrumbs
|
|
159
|
+
if ( kTest ) Object.entries( theData ).forEach( ([k,v]) => v['text'] = render ( v, name, helpers ) )
|
|
160
|
+
else theData['text'] = render ( theData, name, helpers )
|
|
161
|
+
break
|
|
162
|
+
} // switch renderDataType
|
|
163
|
+
break;
|
|
164
|
+
case 'extendedRender':
|
|
165
|
+
// TODO: Test extendedRender
|
|
166
|
+
const isValid = typeof helpers[name] === 'function'; // Render could be a function and template.
|
|
167
|
+
if ( isValid ) {
|
|
168
|
+
nestedData[level] = helpers[name]( nestedData[0] )
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// TODO: Error...
|
|
172
|
+
}
|
|
173
|
+
break
|
|
174
|
+
case 'mix':
|
|
175
|
+
if ( name === '' ) { // when is anonymous mixing helper
|
|
176
|
+
switch ( dataType ) {
|
|
177
|
+
case 'object':
|
|
178
|
+
let kTest = Object.keys ( theData ).find ( k => k.includes ( '/' ) ); // Check if keys are breadcrumbs
|
|
179
|
+
if ( kTest ) Object.entries( theData ).forEach( ([k,v]) => nestedData[level][k] = v['text'] )
|
|
180
|
+
else nestedData[level] = theData['text']
|
|
181
|
+
for ( let i=level-1; i >= 0; i-- ) {
|
|
182
|
+
nestedData[i] = walk ({data:nestedData[i], objectCallback:check})
|
|
183
|
+
}
|
|
184
|
+
function check ({ value, breadcrumbs }) {
|
|
185
|
+
if ( nestedData[level][breadcrumbs] ) return nestedData[level][breadcrumbs]
|
|
186
|
+
return value
|
|
187
|
+
} // check
|
|
188
|
+
break
|
|
189
|
+
case 'array':
|
|
190
|
+
nestedData[level] = theData.map ( x => {
|
|
191
|
+
let xType = _defineDataType ( x );
|
|
192
|
+
if ( xType === 'object' ) return x.text
|
|
193
|
+
return x
|
|
194
|
+
})
|
|
195
|
+
.filter ( x => x != null )
|
|
196
|
+
.join ( '' )
|
|
197
|
+
break
|
|
198
|
+
} // switch dataType
|
|
199
|
+
} // if name === ''
|
|
200
|
+
else nestedData[level] = helpers[name]( theData )
|
|
201
|
+
break
|
|
202
|
+
default:
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
} // for step of actSetup
|
|
206
|
+
let accType = _defineDataType ( nestedData[0] )
|
|
207
|
+
let fineData = nestedData[0]
|
|
208
|
+
|
|
209
|
+
switch ( accType ) {
|
|
210
|
+
case 'primitive':
|
|
211
|
+
if ( fineData == null ) return
|
|
212
|
+
cuts[index] = fineData
|
|
213
|
+
break
|
|
214
|
+
case 'object':
|
|
215
|
+
if (fineData['text'] == null ) return
|
|
216
|
+
cuts[index] = fineData['text']
|
|
217
|
+
break
|
|
218
|
+
case 'array':
|
|
219
|
+
cuts[index] = fineData.join ( '' )
|
|
220
|
+
break
|
|
221
|
+
} // switch accType
|
|
222
|
+
} // else other
|
|
223
|
+
}) // forEach placeholders
|
|
224
|
+
endData.push ( cuts.join ( '' ))
|
|
225
|
+
}) // forEach d
|
|
226
|
+
|
|
227
|
+
if ( topLevelType === 'array' ) return endData
|
|
228
|
+
if (args) return args.reduce ( (acc, fn) => fn ( acc ), endData.join ( '' ) )
|
|
229
|
+
else return endData.join ( '' )
|
|
230
|
+
|
|
231
|
+
} // success func.
|
|
232
|
+
}
|
|
233
|
+
} // build func.
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
export default build
|
|
238
|
+
|
|
239
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import _renderHolder from "./_renderHolder.js"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function render ( theData, name, helpers ) {
|
|
6
|
+
// *** Executes rendering and return the results
|
|
7
|
+
if ( theData instanceof Object ) { // Make sure all properties are not objects
|
|
8
|
+
Object.entries ( theData ).forEach ( ([key, value]) => {
|
|
9
|
+
if ( value instanceof Object ) theData[key] = value['text']
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
function setRenderData ( d={} ) {
|
|
13
|
+
if ( typeof d === 'string' ) return { text: d }
|
|
14
|
+
else return d
|
|
15
|
+
} // setRenderData func.
|
|
16
|
+
const isRenderFunction = typeof helpers[name] === 'function'; // Render could be a function or template.
|
|
17
|
+
theData = setRenderData ( theData )
|
|
18
|
+
if ( isRenderFunction ) return helpers[name]( theData )
|
|
19
|
+
else return _renderHolder ( helpers[name], theData )
|
|
20
|
+
} // render func.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export default render
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chopTemplates from'../src/methods/_chopTemplates.js'
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
|
|
4
|
+
describe ( 't-templates: chop', () => {
|
|
5
|
+
|
|
6
|
+
it ( 'Chop', () => {
|
|
7
|
+
const
|
|
8
|
+
str = 'Hello, {{name:ul,li,a}}! Say: {{shout}}, {{shout}}!'
|
|
9
|
+
, settings = {
|
|
10
|
+
TG_PRX: '{{'
|
|
11
|
+
, TG_SFX: '}}'
|
|
12
|
+
, TG_SIZE_P: 2
|
|
13
|
+
, TG_SIZE_S: 2
|
|
14
|
+
}
|
|
15
|
+
;
|
|
16
|
+
const tpl = chopTemplates ( settings);
|
|
17
|
+
const res = tpl(str);
|
|
18
|
+
|
|
19
|
+
expect(res).to.be.deep.equal ([
|
|
20
|
+
'Hello, ', '{{name:ul,li,a}}', '! Say: ', '{{shout}}', ', ', '{{shout}}', '!'
|
|
21
|
+
])
|
|
22
|
+
}) // it chop
|
|
23
|
+
|
|
24
|
+
}) // Describe
|