spyne 0.20.17 → 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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Spyne 0.20.17
2
+ * Spyne 0.21.1
3
3
  * https://spynejs.org
4
4
  *
5
5
  * @license
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  /*!
18
- * spynejs 0.20.17
18
+ * spynejs 0.21.1
19
19
  * https://spynejs.org
20
20
  * (c) 2017-present Frank Batista
21
21
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spyne",
3
- "version": "0.20.17",
3
+ "version": "0.21.1",
4
4
  "description": "Reactive Real-DOM Framework for Advanced Javascript applications",
5
5
  "sideEffects": true,
6
6
  "main": "./lib/spyne.esm.js",
@@ -24,12 +24,13 @@
24
24
  "dev": "webpack --config webpack.config.js --mode development",
25
25
  "stats": "webpack --profile --json --optimize-minimize --mode production --env build > stats.json",
26
26
  "docs": "jsdoc --readme README.md -d docs -c docs/jsdoc.config.json -r",
27
- "esdocs": "esdoc -d esdocs -c esdocs/esdoc.config.json -r",
27
+ "esdocs": "esdoc -d esdocs -c esdocs/esdoc.config.json -r",
28
28
  "test": "karma start karma.conf.js",
29
29
  "test:single": "karma start karma.conf.js --single-run",
30
30
  "watch:test": "npm test -- --auto-watch --no-single-run",
31
31
  "test:mocha": "mocha tests",
32
32
  "lint": "eslint 'src/spyne/**' --fix",
33
+ "verify:dist": "test -f lib/spyne.esm.js && test -f lib/spyne.umd.js",
33
34
  "prepare": "husky"
34
35
  },
35
36
  "repository": {
@@ -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.20.17'
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.20.17
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', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'meta', 'link'],
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 SAFE_FOR_RICH_TEXT.SAFE_URL_PATTERN.test(val) ? m : `${attr}=""`
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 {{&nbsp;&nbsp;&nbsp;}} 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}} &nbsp;&nbsp;{{.}}&nbsp;&nbsp; {{/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 {{#}} &nbsp;&nbsp;{{ }}&nbsp;&nbsp; {{/}} 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 {{#}} &nbsp;&nbsp;{{ . }}&nbsp;&nbsp; {{/}} 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="&lt;li/);
80
+ });
81
+ });
82
+ });