spyne 0.20.19 → 0.21.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/lib/spyne.esm.js +3 -3
- package/lib/spyne.esm.js.map +1 -1
- package/lib/spyne.umd.js +2 -2
- package/lib/spyne.umd.js.LICENSE.txt +2 -2
- package/package.json +1 -1
- package/src/spyne/spyne-app.js +2 -7
- package/src/spyne/utils/sanitize-data.js +13 -7
- package/src/spyne/utils/spyne-app-properties.js +1 -53
- package/src/spyne/views/dom-element-template.js +1 -150
- package/src/tests/views/dom-el-template-proxy.test.js +70 -2
package/package.json
CHANGED
package/src/spyne/spyne-app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { sanitizeHTMLConfigure } from './utils/sanitize-html.js'
|
|
|
6
6
|
import { sanitizeDataConfigure } from './utils/sanitize-data.js'
|
|
7
7
|
|
|
8
8
|
const _channels = new ChannelsMap()
|
|
9
|
-
const version = '0.
|
|
9
|
+
const version = '0.21.1'
|
|
10
10
|
|
|
11
11
|
class SpyneApplication {
|
|
12
12
|
/**
|
|
@@ -32,8 +32,6 @@ class SpyneApplication {
|
|
|
32
32
|
|
|
33
33
|
constructor() {
|
|
34
34
|
this.version = version
|
|
35
|
-
|
|
36
|
-
// console.log('spyne app created')
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
get channels() {
|
|
@@ -43,7 +41,7 @@ class SpyneApplication {
|
|
|
43
41
|
init(config = {}, testMode = false) {
|
|
44
42
|
// this.channels = new ChannelsMap();
|
|
45
43
|
/*!
|
|
46
|
-
* Spyne 0.
|
|
44
|
+
* Spyne 0.21.1
|
|
47
45
|
* https://spynejs.org
|
|
48
46
|
*
|
|
49
47
|
* @license
|
|
@@ -59,9 +57,6 @@ class SpyneApplication {
|
|
|
59
57
|
*/
|
|
60
58
|
/* eslint-disable */
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
60
|
if(SpyneAppProperties.initialized === true){
|
|
66
61
|
if (testMode){
|
|
67
62
|
return 'The Spyne Application has already been initialized!';
|
|
@@ -15,15 +15,16 @@ const SAFE_FOR_RICH_TEXT = {
|
|
|
15
15
|
'blockquote', 'pre', 'code', 'span', 'div', 'section', 'article', 'aside',
|
|
16
16
|
'header', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
17
17
|
'table', 'thead', 'tbody', 'tr', 'td', 'th', 'img', 'figure', 'figcaption',
|
|
18
|
-
'video', 'source'
|
|
18
|
+
'video', 'source', 'iframe', 'input', 'button', 'label', 'link'
|
|
19
19
|
],
|
|
20
|
+
ADD_TAGS: ['iframe', 'input', 'button', 'link'],
|
|
20
21
|
ALLOWED_ATTR: [
|
|
21
|
-
'href', 'src', 'alt', 'title', 'width', 'height', 'style', 'target', 'rel',
|
|
22
|
+
'href', 'src', 'alt', 'class', 'title', 'width', 'height', 'style', 'target', 'rel',
|
|
22
23
|
'controls', 'poster', 'type', 'rows', 'cols'
|
|
23
24
|
],
|
|
24
25
|
ALLOW_DATA_ATTR: true,
|
|
25
26
|
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange'],
|
|
26
|
-
FORBID_TAGS: ['script', '
|
|
27
|
+
FORBID_TAGS: ['script', 'object', 'embed', 'meta', 'link'],
|
|
27
28
|
SAFE_URL_PATTERN: /^(https?:|mailto:|tel:|data:image\/)/i
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -38,7 +39,7 @@ const SAFE_FOR_APP = {
|
|
|
38
39
|
* ------------------------------------------- */
|
|
39
40
|
const resolveDefaultMode = () => {
|
|
40
41
|
try {
|
|
41
|
-
const mode = SpyneAppProperties?.mode
|
|
42
|
+
const mode = SpyneAppProperties?.mode || process?.env?.NODE_ENV
|
|
42
43
|
if (mode === 'authoring' || mode === 'development') return 'richtext'
|
|
43
44
|
} catch (e) {
|
|
44
45
|
// Fallback: assume production strict mode
|
|
@@ -48,6 +49,10 @@ const resolveDefaultMode = () => {
|
|
|
48
49
|
|
|
49
50
|
/* --------------------------------------------- */
|
|
50
51
|
function makeSanitizeFn(mode = 'app') {
|
|
52
|
+
if (process?.env?.NODE_ENV === 'development') {
|
|
53
|
+
mode = 'richtext'
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
const cfg = mode === 'richtext' ? SAFE_FOR_RICH_TEXT : SAFE_FOR_APP
|
|
52
57
|
|
|
53
58
|
return (html) => {
|
|
@@ -57,7 +62,7 @@ function makeSanitizeFn(mode = 'app') {
|
|
|
57
62
|
if (mode === 'richtext') {
|
|
58
63
|
clean = clean.replace(/<(a|img)\b([^>]+?)>/gi, (match) => {
|
|
59
64
|
return match.replace(/\b(href|src)="([^"]*)"/gi, (m, attr, val) => {
|
|
60
|
-
return
|
|
65
|
+
return m
|
|
61
66
|
})
|
|
62
67
|
})
|
|
63
68
|
}
|
|
@@ -114,6 +119,7 @@ const sanitizeData = (data, opts = {}) => {
|
|
|
114
119
|
if (!isConfigured) {
|
|
115
120
|
throw new Error('sanitizeData is not configured. Call sanitizeDataConfigure() first.')
|
|
116
121
|
}
|
|
122
|
+
// return data;
|
|
117
123
|
return _sanitizeData(data, opts)
|
|
118
124
|
}
|
|
119
125
|
|
|
@@ -135,8 +141,8 @@ const sanitizeEventTarget = (el, mode) => {
|
|
|
135
141
|
if (!el) return
|
|
136
142
|
const effectiveMode = mode || resolveDefaultMode()
|
|
137
143
|
const sanitizeStr = makeSanitizeFn(effectiveMode)
|
|
138
|
-
const UNSAFE_RE = /<(?:script|iframe|object|embed|form|input|button|link|meta)[^>]*>|on\w+=|javascript:/i
|
|
139
|
-
|
|
144
|
+
// const UNSAFE_RE = /<(?:script|iframe|object|embed|form|input|button|link|meta)[^>]*>|on\w+=|javascript:/i
|
|
145
|
+
const UNSAFE_RE = /<(?:script|object|embed|form|input|button|link|meta)[^>]*>|on\w+=|javascript:/i
|
|
140
146
|
if (typeof el.value === 'string' && UNSAFE_RE.test(el.value)) {
|
|
141
147
|
const clean = sanitizeStr(el.value)
|
|
142
148
|
if (clean !== el.value) el.value = clean
|
|
@@ -277,62 +277,10 @@ class SpyneAppPropertiesClass {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
setCMSProxyMethod(fn){
|
|
280
|
-
// this.formatTemplateForProxyData = fn;
|
|
281
280
|
this.enableCMSProxies = true;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// SpyneJS Enterprise Code Start
|
|
285
|
-
formatTemplateForProxyData(tmpl) {
|
|
286
|
-
const isPrimitiveTag = str => /({{\.\*?}})/.test(str);
|
|
287
|
-
|
|
288
|
-
const reLines2 = /(({{(?!loopNum|loopIndex))(.*?)(}}))/gm
|
|
289
|
-
const reStr = '(?<=>)(.*?)(({{)(.*)(}}))'
|
|
290
|
-
|
|
291
|
-
const reLines1 = new RegExp(reStr, 'gm')
|
|
292
|
-
|
|
293
|
-
const formatCmsParam = (str) => {
|
|
294
|
-
const isArrPrimitive = isPrimitiveTag(str)
|
|
295
|
-
|
|
296
|
-
const getKeyAndDataId = (s) => {
|
|
297
|
-
const re = /({{)([\w_]+(\.))*?(\w+)(}})/gm // this is old, will be deprecated.
|
|
298
|
-
const reNestedData = /^\{\{([^}]+?)\.[^.}]+}}$/gm
|
|
299
|
-
|
|
300
|
-
let dataId = '__cms__dataId'
|
|
301
|
-
const key = isArrPrimitive ? '{{loopIndex}}' : String(s).replace(re, '$4')
|
|
302
|
-
if (/(\w\.)/.test(s)) {
|
|
303
|
-
dataId = String(s).replace(reNestedData, '$1.') + dataId
|
|
304
|
-
}
|
|
305
|
-
return { key, dataId }
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const { key, dataId } = getKeyAndDataId(str)
|
|
281
|
+
this.formatTemplateForProxyData = fn;
|
|
309
282
|
|
|
310
|
-
let prefix = `<spyne-cms-item data-cms-id="{{${dataId}}}" data-cms-key="${key}"`
|
|
311
|
-
|
|
312
|
-
// IF AN ARRAY PRIMITIVE CHECK FOR ORIGINAL ARRAY POSITION
|
|
313
|
-
if (isArrPrimitive) {
|
|
314
|
-
prefix += ' data-cms-orig-key="{{origKey}}"'
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// CREATE PROXY TAG
|
|
318
|
-
const suffix = '</spyne-cms-item>'
|
|
319
|
-
const param = isArrPrimitive ? '{{spyneLoopKey}}' : str
|
|
320
|
-
return `${prefix}><spyne-cms-item-hitbox></spyne-cms-item-hitbox><spyne-cms-item-text>${param}</spyne-cms-item-text>${suffix}`
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const reSwapTags = (str, m1, m2, m3) => {
|
|
324
|
-
return formatCmsParam(str)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const reMethod = (str, m1, m2, m3, m4) => {
|
|
328
|
-
const m2Updated = String(m2).replace(reLines2, reSwapTags)
|
|
329
|
-
return `${m1}${m2Updated}`
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return String(tmpl).replace(reLines1, reMethod)
|
|
333
283
|
}
|
|
334
|
-
// SpyneJS Enterprise Code End
|
|
335
|
-
|
|
336
284
|
|
|
337
285
|
tempGetChannelsInstance() {
|
|
338
286
|
|
|
@@ -2,152 +2,13 @@ import { includes, __, ifElse, path, prop, reject, is, isNil, isEmpty } from 'ra
|
|
|
2
2
|
import sanitizeHTML from '../utils/sanitize-html.js'
|
|
3
3
|
import { SpyneAppProperties } from '../utils/spyne-app-properties.js'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @module DomElTemplate
|
|
7
|
-
* @type util
|
|
8
|
-
*
|
|
9
|
-
* @constructor
|
|
10
|
-
* @param {String|HTMLElement} template
|
|
11
|
-
* @param {Object} data
|
|
12
|
-
*
|
|
13
|
-
* @desc DomElement uses this class when rendering templates.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* TITLE["<h4>Accessing properties using a double bracket {{ }} expression</h4>"]
|
|
17
|
-
* const data = {{name:"World}};
|
|
18
|
-
* const template = `<p>Hello {{name}}!</p>`;
|
|
19
|
-
*
|
|
20
|
-
* this.props.el.appendChild(new
|
|
21
|
-
* DocElTemplate(template,data).renderDocFrag());
|
|
22
|
-
*
|
|
23
|
-
* // Outputs
|
|
24
|
-
* <p>Hello World!</p>
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* TITLE["<h4>Looping through an an array of objects using loop {{#loopProperty}} {{property}} {{/loopProperty}} expression</h4>"]
|
|
30
|
-
* const data = {animals: [
|
|
31
|
-
* {animal: 'cat', sound: 'meow'},
|
|
32
|
-
* {animal: 'dog', sound: 'woof'},
|
|
33
|
-
* {animal: 'bird', sound: tweet'}
|
|
34
|
-
* ]};
|
|
35
|
-
*
|
|
36
|
-
* const template = `
|
|
37
|
-
* <ul>
|
|
38
|
-
* {{#animals}}
|
|
39
|
-
* <li>The {{animal}} says '{{sound}}'.</li>
|
|
40
|
-
* {{/animals}}
|
|
41
|
-
* </ul>`;
|
|
42
|
-
*
|
|
43
|
-
* this.props.el.appendChild(new
|
|
44
|
-
* DocElTemplate(template,data).renderDocFrag());
|
|
45
|
-
*
|
|
46
|
-
* // Outputs
|
|
47
|
-
* <ul>
|
|
48
|
-
* <li>The cat says 'meow'.</li>
|
|
49
|
-
* <li>The dog says 'woof'.</li>
|
|
50
|
-
* <li>The bird says 'tweet'.</li>
|
|
51
|
-
* </ul>
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* TITLE["<h4>Looping through list items in an array of using {{#arrName}} {{.}} {{/arrName}} expression</h4>"]
|
|
57
|
-
* const data = {name: 'Jane', pets: ['Milo','Luna','Kiki']};
|
|
58
|
-
* const template = "<article>
|
|
59
|
-
* <h3>Welcome, {{name}}, your pets are:</h3>
|
|
60
|
-
* <ul>{{#pets}}
|
|
61
|
-
* <li>{{.}}, </li>
|
|
62
|
-
* {{/pets}}
|
|
63
|
-
* </ul></article>";
|
|
64
|
-
*
|
|
65
|
-
* this.props.el.appendChild(new
|
|
66
|
-
* DocElTemplate(template,data).renderDocFrag());
|
|
67
|
-
*
|
|
68
|
-
* // Outputs
|
|
69
|
-
* <article>
|
|
70
|
-
* <h3>Welcome Jane, your pets are:</h3>
|
|
71
|
-
* <ul>
|
|
72
|
-
* <li>Milo</li>
|
|
73
|
-
* <li>Luna</li>
|
|
74
|
-
* <li>Kiki</li>
|
|
75
|
-
* </ul>
|
|
76
|
-
* </article>
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* TITLE["<h4>Looping through an array with no property name using {{#}} {{ }} {{/}} expression</h4>"]
|
|
81
|
-
* const data = [
|
|
82
|
-
* {item: 'Cookie', calories: 142},
|
|
83
|
-
* {item: 'Apple', calories: 95},
|
|
84
|
-
* {item: 'Cheese', calories: 113}];
|
|
85
|
-
*
|
|
86
|
-
* const template = `
|
|
87
|
-
* <article>
|
|
88
|
-
* <h3>Snacks:</h3>
|
|
89
|
-
* <ul>
|
|
90
|
-
* {{#}}
|
|
91
|
-
* <li>{{item}}, calories: {{calories}}</li>
|
|
92
|
-
* {{/}}
|
|
93
|
-
* </ul>
|
|
94
|
-
* </article>
|
|
95
|
-
* `;
|
|
96
|
-
*
|
|
97
|
-
* this.props.el.appendChild(new
|
|
98
|
-
* DocElTemplate(template,data).renderDocFrag());
|
|
99
|
-
*
|
|
100
|
-
* // Outputs
|
|
101
|
-
* <article>
|
|
102
|
-
* <h3>Snacks</h3>
|
|
103
|
-
* <ul>
|
|
104
|
-
* <li>Cookie, calories: 142</li>
|
|
105
|
-
* <li>Apple, calories: 95</li>
|
|
106
|
-
* <li>Cheese, calories: 113</li>
|
|
107
|
-
* </ul>
|
|
108
|
-
* </article>
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* TITLE["<h4>Looping through an array with no property name using {{#}} {{ . }} {{/}} expression</h4>"]
|
|
113
|
-
* const data = ['Tyrion', 'Arya', 'Jon Snow', 'Sansa'];
|
|
114
|
-
* const template = `
|
|
115
|
-
* <h3>Main GoT Characters</h3>
|
|
116
|
-
* <ul>
|
|
117
|
-
* {{#}}
|
|
118
|
-
* <li>{{.}}</li>
|
|
119
|
-
* {{/}}
|
|
120
|
-
* </ul>';
|
|
121
|
-
*
|
|
122
|
-
* this.props.el.appendChild(new
|
|
123
|
-
* DocElTemplate(template,data).renderDocFrag());
|
|
124
|
-
*
|
|
125
|
-
* //Outputs
|
|
126
|
-
* <h3>Main GoT Characters</h3>
|
|
127
|
-
* <ul>
|
|
128
|
-
* <li>Tyrion</li>
|
|
129
|
-
* <li>Arya</li>
|
|
130
|
-
* <li>Jon Snow</li>
|
|
131
|
-
* <li>Sansa</li>
|
|
132
|
-
* </ul>
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*/
|
|
136
|
-
|
|
137
5
|
export class DomElementTemplate {
|
|
138
6
|
constructor(template, data = {}, opts = {}) {
|
|
139
7
|
this.template = this.formatTemplate(template)
|
|
140
8
|
this.isProxyData = data.__cms__isProxy === true
|
|
141
9
|
this.testMode = opts?.testMode
|
|
142
10
|
|
|
143
|
-
|
|
144
|
-
if (this.isProxyData === true) {
|
|
145
|
-
console.log('PROXY IS ', data.__cms__rootData)
|
|
146
|
-
this.template = DomElementTemplate.formatTemplateForProxyData(this.template)
|
|
147
|
-
}
|
|
148
|
-
*/
|
|
149
|
-
|
|
150
|
-
if (SpyneAppProperties.enableCMSProxies && this.isProxyData === true) {
|
|
11
|
+
if (this.isProxyData === true && SpyneAppProperties.enableCMSProxies) {
|
|
151
12
|
this.template = SpyneAppProperties.formatTemplateForProxyData(this.template)
|
|
152
13
|
}
|
|
153
14
|
|
|
@@ -237,7 +98,6 @@ export class DomElementTemplate {
|
|
|
237
98
|
|
|
238
99
|
renderDocFrag() {
|
|
239
100
|
let html = DomElementTemplate.replaceImgPath(this.finalArr.join(''))
|
|
240
|
-
// html = sanitizeHTML(this.finalArr.join(''))
|
|
241
101
|
if (this.testMode !== true) {
|
|
242
102
|
html = sanitizeHTML(html)
|
|
243
103
|
}
|
|
@@ -286,24 +146,15 @@ export class DomElementTemplate {
|
|
|
286
146
|
}
|
|
287
147
|
|
|
288
148
|
parseTheTmplLoop(str, p1, p2, p3) {
|
|
289
|
-
// const dotConverter = str => `${str.replace(/(\.)/g, '][')}`
|
|
290
149
|
const reDot = /(\.)/gm
|
|
291
150
|
const subStr = p3
|
|
292
|
-
|
|
293
|
-
/* const dataReducer = (acc, str) => {
|
|
294
|
-
acc = acc[str]
|
|
295
|
-
return acc
|
|
296
|
-
} */
|
|
297
|
-
|
|
298
151
|
let elData = DomElementTemplate.getNestedDataReducer(this.templateData, p2)
|
|
299
152
|
|
|
300
153
|
const arrayStringToObjAdapter = (d, str, i) => {
|
|
301
|
-
// IF {{.}} RUN parseString
|
|
302
154
|
if (DomElementTemplate.isPrimitiveTag(str)) {
|
|
303
155
|
return parseString(d, str, i)
|
|
304
156
|
}
|
|
305
157
|
|
|
306
|
-
// CREATE DATA OBJ -- CHECK TO ADD PROXY VALUES
|
|
307
158
|
const createDataObj = () => {
|
|
308
159
|
const spyneLoopKey = d
|
|
309
160
|
const loopIndex = i
|
|
@@ -3,8 +3,6 @@ import { DomElementTemplate } from '../../spyne/views/dom-element-template';
|
|
|
3
3
|
// Evaluate once at the top
|
|
4
4
|
const isPublic = process.env.IS_PUBLIC === true;
|
|
5
5
|
|
|
6
|
-
console.log("IS PUBLIC ",isPublic);
|
|
7
|
-
|
|
8
6
|
|
|
9
7
|
// If not public, skip this test suite altogether
|
|
10
8
|
(isPublic ? describe : describe.skip)('DomElementTemplate Proxy (Public Test)', () => {
|
|
@@ -12,3 +10,73 @@ console.log("IS PUBLIC ",isPublic);
|
|
|
12
10
|
expect(DomElementTemplate.hasOwnProperty('formatTemplateForProxyData')).to.be.false;
|
|
13
11
|
});
|
|
14
12
|
});
|
|
13
|
+
|
|
14
|
+
(!isPublic ? describe : describe.skip)('DomElementTemplate Proxy (Internal Test)', () => {
|
|
15
|
+
describe('formatTemplateForProxyData', () => {
|
|
16
|
+
it('should wrap text-node placeholders like <h1>{{title}}</h1> in <spyne-cms-item>', () => {
|
|
17
|
+
const input = '<h1>{{title}}</h1>';
|
|
18
|
+
const output = DomElementTemplate.formatTemplateForProxyData(input);
|
|
19
|
+
|
|
20
|
+
// wrapper exists
|
|
21
|
+
expect(output).to.include('<spyne-cms-item');
|
|
22
|
+
|
|
23
|
+
// text is preserved inside the CMS text wrapper
|
|
24
|
+
expect(output).to.include(
|
|
25
|
+
'<spyne-cms-item-text>{{title}}</spyne-cms-item-text>'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// structural sanity check
|
|
29
|
+
expect(output).to.include('</spyne-cms-item>');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should NOT wrap attribute placeholders like <img src="{{imgUrl}}"> and keep them intact', () => {
|
|
33
|
+
const input = '<img src="{{imgUrl}}">';
|
|
34
|
+
const output = DomElementTemplate.formatTemplateForProxyData(input);
|
|
35
|
+
|
|
36
|
+
// attributes must remain untouched
|
|
37
|
+
expect(output).to.equal(input);
|
|
38
|
+
expect(output).to.not.include('<spyne-cms-item');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should allow prefixed attr* placeholders inside attributes without CMS wrapping', () => {
|
|
42
|
+
const input = '<img src="{{attrImgSrc}}" alt="{{attrImgAlt}}">';
|
|
43
|
+
const output = DomElementTemplate.formatTemplateForProxyData(input);
|
|
44
|
+
|
|
45
|
+
// attr* placeholders should remain intact
|
|
46
|
+
expect(output).to.include('{{attrImgSrc}}');
|
|
47
|
+
expect(output).to.include('{{attrImgAlt}}');
|
|
48
|
+
|
|
49
|
+
// no CMS proxy tags should be injected
|
|
50
|
+
expect(output).to.not.include('<spyne-cms-item');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should not wrap loop root elements or corrupt list structure', () => {
|
|
54
|
+
const input = `
|
|
55
|
+
<ul>
|
|
56
|
+
{{#items}}
|
|
57
|
+
<li><span>{{title}}</span></li>
|
|
58
|
+
{{/items}}
|
|
59
|
+
</ul>
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const data = {
|
|
63
|
+
__cms__isProxy: true,
|
|
64
|
+
items: [
|
|
65
|
+
{ title: 'A' },
|
|
66
|
+
{ title: 'B' },
|
|
67
|
+
{ title: 'C' }
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const tmpl = new DomElementTemplate(input, data);
|
|
72
|
+
const output = tmpl.renderToString();
|
|
73
|
+
|
|
74
|
+
// list structure must be preserved
|
|
75
|
+
expect(output.match(/<li>/g).length).to.equal(3);
|
|
76
|
+
|
|
77
|
+
// CMS proxy should exist only around text nodes
|
|
78
|
+
expect(output).to.include('<spyne-cms-item');
|
|
79
|
+
expect(output).to.not.match(/data-cms-key="<li/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|