spyne 0.21.4 → 0.22.2

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.
@@ -253,7 +253,7 @@ function ViewStreamSelector(cxt, sel) {
253
253
  * @param {String|HTMLElement} elSel The selector for the element.
254
254
  * @desc Sets the class active HTMLElement from a NodeList.
255
255
  */
256
- selector.setActiveItem = (c, elSel) => {
256
+ selector.setActiveItem2 = (c, elSel) => {
257
257
  const arr = getNodeListArray(cxt, sel)
258
258
  const currentEl = typeof (elSel) === 'string' ? getElOrList(cxt, elSel) : elSel
259
259
  const toggleBool = item => item.isEqualNode(currentEl) ? item.classList.add(c) : item.classList.remove(c)
@@ -266,6 +266,33 @@ function ViewStreamSelector(cxt, sel) {
266
266
  return this
267
267
  }
268
268
 
269
+ selector.setActiveItem = (c, elSel) => {
270
+ const arr = getNodeListArray(cxt, sel)
271
+ const currentEl = typeof elSel === 'string'
272
+ ? getElOrList(cxt, elSel)
273
+ : elSel
274
+
275
+ arr.forEach(item => item.classList.remove(c))
276
+
277
+ if (isNodeElement(currentEl) === true) {
278
+ const matchingEl = Array.from(arr).find(item => item === currentEl)
279
+
280
+ if (matchingEl) {
281
+ matchingEl.classList.add(c)
282
+ } else if (isDevMode() === true) {
283
+ console.warn(
284
+ `Spyne Warning: The selector, ${elSel}, is valid but does not match any item in setActiveItem: ${c}`
285
+ )
286
+ }
287
+ } else if (isDevMode() === true) {
288
+ console.warn(
289
+ `Spyne Warning: The selector, ${elSel}, does not appear to be a valid item in setActiveItem: ${c}`
290
+ )
291
+ }
292
+
293
+ return this
294
+ }
295
+
269
296
  /**
270
297
  *
271
298
  * @function el
@@ -285,3 +285,176 @@ describe('DomElTemplate', () => {
285
285
  expect(result).to.eq('<li>Eu delenit meliore graecis sea. Sit id ubique commune, ius ne recusabo oportere similique, error putant usu ei.</li>')
286
286
  })
287
287
  })
288
+
289
+ // ─────────────────────────────────────────────────────────────────────────
290
+ // One-level nested array loops
291
+ //
292
+ // DomElementTemplate supports exactly one level of nested array loops.
293
+ // These tests verify both the supported case (a loop inside a loop) and
294
+ // the boundary behavior — outer-loop splitting with the backreference
295
+ // regex, and a deliberate warning when deeper nesting is attempted.
296
+ // ─────────────────────────────────────────────────────────────────────────
297
+
298
+ describe('DomElementTemplate — one-level nested loops', () => {
299
+
300
+ it('outer-loop regex correctly pairs {{#name}} with {{/name}} when a different-named inner loop is present', () => {
301
+ // This verifies the \2 backreference fix — without it, the outer match
302
+ // would incorrectly terminate at {{/cells}} and orphan {{/rows}}.
303
+ const template = '<table>{{#rows}}<tr>{{#cells}}<td>{{.}}</td>{{/cells}}</tr>{{/rows}}</table>'
304
+ const matches = template.match(DomElementTemplate.findTmplLoopsRE())
305
+ expect(matches).to.have.lengthOf(1)
306
+ expect(matches[0]).to.equal('{{#rows}}<tr>{{#cells}}<td>{{.}}</td>{{/cells}}</tr>{{/rows}}')
307
+ })
308
+
309
+ it('renders nested array-of-arrays (the canonical table case)', () => {
310
+ const data = {
311
+ rows: [
312
+ { cells: ['a1', 'a2'] },
313
+ { cells: ['b1', 'b2'] }
314
+ ]
315
+ }
316
+ const template = '<table><tbody>{{#rows}}<tr>{{#cells}}<td>{{.}}</td>{{/cells}}</tr>{{/rows}}</tbody></table>'
317
+ const domElTemplate = new DomElementTemplate(template, data, { testMode: true })
318
+ const render = domElTemplate.renderDocFrag()
319
+
320
+ const rows = render.firstElementChild.querySelectorAll('tr')
321
+ expect(rows).to.have.lengthOf(2)
322
+
323
+ const firstRowCells = rows[0].querySelectorAll('td')
324
+ expect(firstRowCells).to.have.lengthOf(2)
325
+ expect(firstRowCells[0].innerText).to.equal('a1')
326
+ expect(firstRowCells[1].innerText).to.equal('a2')
327
+
328
+ const secondRowCells = rows[1].querySelectorAll('td')
329
+ expect(secondRowCells[0].innerText).to.equal('b1')
330
+ expect(secondRowCells[1].innerText).to.equal('b2')
331
+ })
332
+
333
+ it('renders nested array-of-objects where each outer item has an inner array of objects', () => {
334
+ const data = {
335
+ sections: [
336
+ {
337
+ title: 'Numbers',
338
+ entries: [
339
+ { term: 'one', definition: 'the first number' },
340
+ { term: 'two', definition: 'after one' }
341
+ ]
342
+ },
343
+ {
344
+ title: 'Letters',
345
+ entries: [
346
+ { term: 'a', definition: 'the first letter' }
347
+ ]
348
+ }
349
+ ]
350
+ }
351
+ const template =
352
+ '<div>{{#sections}}<section><h2>{{title}}</h2>{{#entries}}<p>{{term}}: {{definition}}</p>{{/entries}}</section>{{/sections}}</div>'
353
+
354
+ const domElTemplate = new DomElementTemplate(template, data, { testMode: true })
355
+ const render = domElTemplate.renderDocFrag()
356
+
357
+ const sections = render.firstElementChild.querySelectorAll('section')
358
+ expect(sections).to.have.lengthOf(2)
359
+
360
+ expect(sections[0].querySelector('h2').innerText).to.equal('Numbers')
361
+ const firstEntries = sections[0].querySelectorAll('p')
362
+ expect(firstEntries).to.have.lengthOf(2)
363
+ expect(firstEntries[0].innerText).to.equal('one: the first number')
364
+ expect(firstEntries[1].innerText).to.equal('two: after one')
365
+
366
+ expect(sections[1].querySelector('h2').innerText).to.equal('Letters')
367
+ const secondEntries = sections[1].querySelectorAll('p')
368
+ expect(secondEntries).to.have.lengthOf(1)
369
+ expect(secondEntries[0].innerText).to.equal('a: the first letter')
370
+ })
371
+
372
+ it('renders outer-level variables alongside an inner loop in the same iteration body', () => {
373
+ // Confirms that {{title}} at the outer scope is still resolved after the
374
+ // inner loop has been expanded. Regression guard for the ordering of
375
+ // inner-loop processing vs. outer-variable substitution.
376
+ const data = {
377
+ groups: [
378
+ { title: 'Group A', items: ['one', 'two'] },
379
+ { title: 'Group B', items: ['three'] }
380
+ ]
381
+ }
382
+ const template =
383
+ '<div>{{#groups}}<h3>{{title}}</h3><ul>{{#items}}<li>{{.}}</li>{{/items}}</ul>{{/groups}}</div>'
384
+
385
+ const domElTemplate = new DomElementTemplate(template, data, { testMode: true })
386
+ const render = domElTemplate.renderDocFrag()
387
+
388
+ const headings = render.firstElementChild.querySelectorAll('h3')
389
+ expect(headings[0].innerText).to.equal('Group A')
390
+ expect(headings[1].innerText).to.equal('Group B')
391
+
392
+ const lists = render.firstElementChild.querySelectorAll('ul')
393
+ expect(lists[0].querySelectorAll('li')).to.have.lengthOf(2)
394
+ expect(lists[0].querySelectorAll('li')[0].innerText).to.equal('one')
395
+ expect(lists[1].querySelectorAll('li')).to.have.lengthOf(1)
396
+ expect(lists[1].querySelectorAll('li')[0].innerText).to.equal('three')
397
+ })
398
+
399
+ it('renders an empty outer section to nothing (empty array)', () => {
400
+ const data = { rows: [] }
401
+ const template = '<table>{{#rows}}<tr>{{#cells}}<td>{{.}}</td>{{/cells}}</tr>{{/rows}}</table>'
402
+ const domElTemplate = new DomElementTemplate(template, data, { testMode: true })
403
+ const result = domElTemplate.renderToString()
404
+ expect(result).to.equal('<table></table>')
405
+ })
406
+
407
+ it('renders an empty inner section to nothing while still iterating the outer section', () => {
408
+ const data = {
409
+ rows: [
410
+ { cells: ['a1', 'a2'] },
411
+ { cells: [] },
412
+ { cells: ['c1'] }
413
+ ]
414
+ }
415
+ const template = '<div>{{#rows}}<p>{{#cells}}<span>{{.}}</span>{{/cells}}</p>{{/rows}}</div>'
416
+ const domElTemplate = new DomElementTemplate(template, data, { testMode: true })
417
+ const render = domElTemplate.renderDocFrag()
418
+
419
+ const paragraphs = render.firstElementChild.querySelectorAll('p')
420
+ expect(paragraphs).to.have.lengthOf(3)
421
+ expect(paragraphs[0].querySelectorAll('span')).to.have.lengthOf(2)
422
+ expect(paragraphs[1].querySelectorAll('span')).to.have.lengthOf(0)
423
+ expect(paragraphs[2].querySelectorAll('span')).to.have.lengthOf(1)
424
+ })
425
+
426
+ it('preserves the single-level (non-nested) case — a table template without an inner loop still renders', () => {
427
+ // Regression guard that the inner-loop detection branch does not break
428
+ // the hot path for templates that have no inner loop at all.
429
+ const data = { items: [{ name: 'first' }, { name: 'second' }] }
430
+ const template = '<ul>{{#items}}<li>{{name}}</li>{{/items}}</ul>'
431
+ const domElTemplate = new DomElementTemplate(template, data, { testMode: true })
432
+ const render = domElTemplate.renderDocFrag()
433
+
434
+ const lis = render.firstElementChild.querySelectorAll('li')
435
+ expect(lis).to.have.lengthOf(2)
436
+ expect(lis[0].innerText).to.equal('first')
437
+ expect(lis[1].innerText).to.equal('second')
438
+ })
439
+
440
+ it('two-level nested loops produce sensible output (deeper loop stripped, no crash)', () => {
441
+ // Deeper nesting than one level is deliberately unsupported. This test
442
+ // confirms the engine degrades gracefully: the outer and inner loops
443
+ // render, the third-level loop tags are stripped so they don't emit as
444
+ // garbled variables, and the test does not throw.
445
+ const data = {
446
+ a: [
447
+ { b: [{ c: ['x', 'y'] }] }
448
+ ]
449
+ }
450
+ const template = '<div>{{#a}}{{#b}}{{#c}}<span>{{.}}</span>{{/c}}{{/b}}{{/a}}</div>'
451
+
452
+ // Should not throw. Output shape is implementation-defined for the
453
+ // unsupported deeper loop; the important properties are:
454
+ // (1) no crash, (2) no literal "{{" or "}}" tokens leaking into output.
455
+ const domElTemplate = new DomElementTemplate(template, data, { testMode: true })
456
+ const result = domElTemplate.renderToString()
457
+ expect(result).to.not.include('{{')
458
+ expect(result).to.not.include('}}')
459
+ })
460
+ })