@teleporthq/teleport-uidl-resolver 0.43.69 → 0.43.71

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.
@@ -0,0 +1,71 @@
1
+ import { insertLinks } from '../../src/resolvers/abilities/utils'
2
+ import { elementNode } from '@teleporthq/teleport-uidl-builders'
3
+ import { UIDLElementNode, UIDLNavLinkNode } from '@teleporthq/teleport-types'
4
+
5
+ /**
6
+ * The editor exports an element bound to a data source as an ELEMENT whose
7
+ * content is the data provider, rendering the element itself inside it. A
8
+ * details-page link on that element reads its record off the provider, and was
9
+ * dropped: walked as a plain element the wrapper looked empty, so the link
10
+ * inside it was never resolved and the published element did not navigate.
11
+ */
12
+
13
+ const detailsLink: UIDLNavLinkNode = {
14
+ type: 'navlink',
15
+ content: {
16
+ // tslint:disable-next-line:no-invalid-template-strings
17
+ routeName: { type: 'expr', content: '`/page/${Templates_data_data?.items?.[3]?.slug}`' },
18
+ },
19
+ }
20
+
21
+ const providerWrapping = (child: UIDLElementNode, type: string): UIDLElementNode =>
22
+ ({
23
+ type: 'element',
24
+ content: {
25
+ type,
26
+ content: {
27
+ elementType: 'DataProvider',
28
+ renderPropIdentifier: 'Templates_data_data',
29
+ resourceDefinition: {
30
+ type: 'external-data-source',
31
+ dataSourceId: 'ds-rest',
32
+ tableName: 'data',
33
+ dataSourceType: 'rest-api',
34
+ },
35
+ nodes: {
36
+ success: { type: 'element', content: { elementType: 'fragment', children: [child] } },
37
+ },
38
+ },
39
+ children: [],
40
+ },
41
+ } as unknown as UIDLElementNode)
42
+
43
+ const linkedChildOf = (page: UIDLElementNode): UIDLElementNode => {
44
+ const wrapper = page.content.children[0] as unknown as {
45
+ content: { content: { nodes: { success: UIDLElementNode } } }
46
+ }
47
+ return wrapper.content.content.nodes.success.content.children[0] as UIDLElementNode
48
+ }
49
+
50
+ describe('insertLinks — an element wrapping a data provider', () => {
51
+ it.each(['data-source-item', 'data-source-list'])(
52
+ 'resolves the link of the element a %s renders',
53
+ (type) => {
54
+ const linked = elementNode('text')
55
+ linked.content.abilities = { link: detailsLink }
56
+ const page = elementNode('container', {}, [providerWrapping(linked, type)])
57
+
58
+ const resolved = linkedChildOf(insertLinks(page, {}, false))
59
+
60
+ expect(resolved.content.elementType).toBe('navlink')
61
+ expect(resolved.content.attrs.transitionTo).toEqual(detailsLink.content.routeName)
62
+ }
63
+ )
64
+
65
+ it('leaves an element with no link inside the provider as it was', () => {
66
+ const plain = elementNode('text')
67
+ const page = elementNode('container', {}, [providerWrapping(plain, 'data-source-item')])
68
+
69
+ expect(linkedChildOf(insertLinks(page, {}, false)).content.elementType).toBe('text')
70
+ })
71
+ })
@@ -42,6 +42,54 @@ describe('insertLink', () => {
42
42
  expect(result.content.attrs.url.content).toBe(link.content.url.content)
43
43
  })
44
44
 
45
+ // Run 10f154ff (Atelier Luzo homepage rail): the page wrote
46
+ // `<div class="surface-media"><a><img></a></div>` and the design system sizes
47
+ // that image through `.surface-media > a > img`. The link came out as
48
+ // `<a><div><img></div></a>`: the extra <div> broke the child chain, the image
49
+ // rendered at its natural 2132 px and blew the rail's card up. The canvas,
50
+ // which does not insert the wrapper, looked right.
51
+ it('a container that is nothing but the link becomes the anchor itself', () => {
52
+ const image = elementNode('image', { src: { type: 'static', content: '/a.jpg' } })
53
+ const bare = elementNode('container', {}, [image])
54
+ bare.content.abilities = { link: urlMockedDefinition() as UIDLURLLinkNode }
55
+ bare.content.attrs = {
56
+ 'data-teleport-link-pending': { type: 'static', content: 'Project Details' },
57
+ }
58
+ const well = elementNode('container', {}, [bare])
59
+
60
+ const result = insertLinks(well, {}, false)
61
+ const anchor = result.content.children[0] as UIDLElementNode
62
+
63
+ expect(anchor.content.elementType).toBe('link')
64
+ expect(anchor.content.attrs.url).toBeDefined()
65
+ expect(anchor.content.attrs['data-teleport-link-pending'].content).toBe('Project Details')
66
+ // The image is the anchor's own child: no element in between.
67
+ expect((anchor.content.children[0] as UIDLElementNode).content.elementType).toBe('image')
68
+ expect(anchor.content.style?.display).toBeUndefined()
69
+ })
70
+
71
+ it('still wraps an element that has a box of its own: a style, a class, an event or a runtime attribute', () => {
72
+ const link = urlMockedDefinition() as UIDLURLLinkNode
73
+ const styled = elementNode('container')
74
+ styled.content.style = { padding: { type: 'static', content: '8px' } }
75
+ const classed = elementNode('container')
76
+ classed.content.referencedStyles = {
77
+ card: { type: 'style-map', content: { mapType: 'project-referenced', referenceId: 'card' } },
78
+ } as never
79
+ const bound = elementNode('container')
80
+ bound.content.attrs = { 'data-scroll-bind': { type: 'static', content: '[]' } }
81
+ const clickable = elementNode('container')
82
+ clickable.content.events = { click: [] }
83
+
84
+ for (const node of [styled, classed, bound, clickable]) {
85
+ node.content.abilities = { link }
86
+ const result = insertLinks(elementNode('container', {}, [node]), {}, false)
87
+ const wrapper = result.content.children[0] as UIDLElementNode
88
+ expect(wrapper.content.elementType).toBe('link')
89
+ expect((wrapper.content.children[0] as UIDLElementNode).content.elementType).toBe('container')
90
+ }
91
+ })
92
+
45
93
  it('replaces a child', () => {
46
94
  const node = elementNode('container', {}, [
47
95
  elementNode('container'),
@@ -350,7 +398,7 @@ describe('insertLink', () => {
350
398
  })
351
399
  })
352
400
 
353
- it('marks the link wrapper display:contents when the flex parent uses a project-referenced style', () => {
401
+ it('a classed div that is a flex item becomes the link itself when the parent is flex through a project class', () => {
354
402
  const child = elementNode('container')
355
403
  child.content.abilities = { link: navlinkMockedDefinition() }
356
404
  child.content.referencedStyles = {
@@ -370,13 +418,54 @@ describe('insertLink', () => {
370
418
  }
371
419
 
372
420
  const result = insertLinks(parent, flexProjectStyleSetOptions('flex'), false)
421
+ const tile = result.content.children[0] as UIDLElementNode
422
+
423
+ // no element in between: the tile is the navlink, with its class and no display of the link's making
424
+ expect(tile.content.elementType).toBe('navlink')
425
+ expect(tile.content.attrs.transitionTo).toBeDefined()
426
+ expect(tile.content.referencedStyles?.TQ_tile).toBeDefined()
427
+ expect(tile.content.style?.display).toBeUndefined()
428
+ expect(tile.content.children ?? []).toHaveLength(0)
429
+ })
430
+
431
+ it('keeps scroll-runtime attributes on the styled element, never on the box-less link wrapper', () => {
432
+ // A heading keeps its tag, so it is the case that still gets a wrapper.
433
+ const child = elementNode('text')
434
+ child.content.semanticType = 'h3'
435
+ child.content.abilities = { link: urlMockedDefinition() }
436
+ child.content.attrs = {
437
+ 'data-scroll-bind': {
438
+ type: 'static',
439
+ content: '[{"prop":"opacity","at":[0,1],"values":[0,1]}]',
440
+ },
441
+ 'data-scroll-bind-rel': { type: 'static', content: '[]' },
442
+ 'data-snap-into-view': { type: 'static', content: 'gentle' },
443
+ 'data-chapter-window': { type: 'static', content: '0.5-1' },
444
+ 'data-analytics': { type: 'static', content: 'cta' },
445
+ }
446
+ const parent = elementNode('container', {}, [child])
447
+ parent.content.style = { display: { type: 'static', content: 'flex' } }
448
+
449
+ const result = insertLinks(parent, {}, false)
373
450
  const wrapper = result.content.children[0] as UIDLElementNode
451
+ const styledChild = wrapper.content.children[0] as UIDLElementNode
374
452
 
375
- expect(wrapper.content.elementType).toBe('navlink')
376
453
  expect(wrapper.content.style?.display).toEqual({ type: 'static', content: 'contents' })
377
- // the original styled node stays inside the wrapper, keeping its class
378
- const styledChild = wrapper.content.children[0] as UIDLElementNode
379
- expect(styledChild.content.referencedStyles?.TQ_tile).toBeDefined()
454
+ expect(Object.keys(wrapper.content.attrs)).not.toEqual(
455
+ expect.arrayContaining([
456
+ 'data-scroll-bind',
457
+ 'data-scroll-bind-rel',
458
+ 'data-snap-into-view',
459
+ 'data-chapter-window',
460
+ ])
461
+ )
462
+ expect(styledChild.content.attrs['data-scroll-bind'].content).toContain('opacity')
463
+ expect(styledChild.content.attrs['data-scroll-bind-rel']).toBeDefined()
464
+ expect(styledChild.content.attrs['data-snap-into-view'].content).toBe('gentle')
465
+ expect(styledChild.content.attrs['data-chapter-window'].content).toBe('0.5-1')
466
+ // ordinary data attributes still travel to the anchor
467
+ expect(wrapper.content.attrs['data-analytics'].content).toBe('cta')
468
+ expect(styledChild.content.attrs['data-analytics']).toBeUndefined()
380
469
  })
381
470
 
382
471
  it('does not mark the link wrapper display:contents when the referenced parent is not flex/grid', () => {
@@ -408,6 +497,8 @@ describe('insertLink', () => {
408
497
  stopped matching the editor, which renders no wrapper at all. */
409
498
  const card = elementNode('container')
410
499
  card.content.abilities = { link: navlinkMockedDefinition() }
500
+ // A card has a box of its own; a bare container would become the anchor itself.
501
+ card.content.style = { flex: { type: 'static', content: '0 0 320px' } }
411
502
 
412
503
  const fragment = elementNode('fragment', {}, [card])
413
504
  const grid = elementNode('container', {}, [fragment])
@@ -421,8 +512,23 @@ describe('insertLink', () => {
421
512
 
422
513
  const result = insertLinks(grid, flexProjectStyleSetOptions('flex'), false)
423
514
  const resolvedFragment = result.content.children[0] as UIDLElementNode
424
- const wrapper = resolvedFragment.content.children[0] as UIDLElementNode
425
-
515
+ const linkedCard = resolvedFragment.content.children[0] as UIDLElementNode
516
+
517
+ // The grid was seen through the fragment: the card is its item, so the card
518
+ // itself is the navlink and its sizing never leaves it.
519
+ expect(linkedCard.content.elementType).toBe('navlink')
520
+ expect(linkedCard.content.style?.flex).toEqual({ type: 'static', content: '0 0 320px' })
521
+ expect(linkedCard.content.style?.display).toBeUndefined()
522
+
523
+ // An element that keeps its tag is wrapped, and that wrapper draws no box.
524
+ const heading = elementNode('text')
525
+ heading.content.semanticType = 'h3'
526
+ heading.content.abilities = { link: navlinkMockedDefinition() }
527
+ const headingGrid = elementNode('container', {}, [elementNode('fragment', {}, [heading])])
528
+ headingGrid.content.referencedStyles = grid.content.referencedStyles
529
+ const wrapped = insertLinks(headingGrid, flexProjectStyleSetOptions('flex'), false)
530
+ const wrapper = (wrapped.content.children[0] as UIDLElementNode).content
531
+ .children[0] as UIDLElementNode
426
532
  expect(wrapper.content.elementType).toBe('navlink')
427
533
  expect(wrapper.content.style?.display).toEqual({ type: 'static', content: 'contents' })
428
534
  })
@@ -451,6 +557,236 @@ describe('insertLink', () => {
451
557
  })
452
558
  })
453
559
 
560
+ /* The link rules generated sites already depend on. Each of them was a fix for a
561
+ broken page at some point (flex items collapsing behind the wrapper in 2020,
562
+ a linked root node in 2024, ids and data attributes lost in 2025), so a change
563
+ to how links are emitted has to keep every one of them. */
564
+ describe('link rules that generated sites depend on', () => {
565
+ const linked = (node: UIDLElementNode): UIDLElementNode => {
566
+ node.content.abilities = { link: urlMockedDefinition() as UIDLURLLinkNode }
567
+ return node
568
+ }
569
+ const styledCard = (): UIDLElementNode => {
570
+ const card = elementNode('container')
571
+ card.content.style = { padding: { type: 'static', content: '24px' } }
572
+ return linked(card)
573
+ }
574
+ const withDisplay = (display: string, child: UIDLElementNode): UIDLElementNode => {
575
+ const parent = elementNode('container', {}, [child])
576
+ parent.content.style = { display: { type: 'static', content: display } }
577
+ return parent
578
+ }
579
+
580
+ const linkedHeading = (): UIDLElementNode => {
581
+ const heading = elementNode('text')
582
+ heading.content.semanticType = 'h3'
583
+ heading.content.style = { margin: { type: 'static', content: '0' } }
584
+ return linked(heading)
585
+ }
586
+
587
+ it.each(['flex', 'inline-flex', 'grid', 'inline-grid'])(
588
+ 'a plain div that is an item of a %s parent becomes the link itself: a box-less wrapper cannot take keyboard focus',
589
+ (display) => {
590
+ const result = insertLinks(withDisplay(display, styledCard()), {}, false)
591
+ const card = result.content.children[0] as UIDLElementNode
592
+
593
+ expect(card.content.elementType).toBe('link')
594
+ expect(card.content.attrs.url).toBeDefined()
595
+ expect(card.content.style?.padding).toEqual({ type: 'static', content: '24px' })
596
+ // nothing of the link's making on it: no display, no wrapper marker, no element in between
597
+ expect(card.content.style?.display).toBeUndefined()
598
+ expect(card.content.attrs['data-thq-link-wrapper']).toBeUndefined()
599
+ expect(card.content.children ?? []).toHaveLength(0)
600
+ }
601
+ )
602
+
603
+ it.each(['flex', 'inline-flex', 'grid', 'inline-grid'])(
604
+ 'an element that keeps its tag is wrapped inside a %s parent, and that wrapper draws no box so the element stays the item',
605
+ (display) => {
606
+ const result = insertLinks(withDisplay(display, linkedHeading()), {}, false)
607
+ const wrapper = result.content.children[0] as UIDLElementNode
608
+
609
+ expect(wrapper.content.elementType).toBe('link')
610
+ expect(wrapper.content.style?.display).toEqual({ type: 'static', content: 'contents' })
611
+ // it says so, which is how the reset stylesheet finds it to paint a focus ring on its child
612
+ expect(wrapper.content.attrs['data-thq-link-wrapper']).toEqual({
613
+ type: 'static',
614
+ content: 'true',
615
+ })
616
+ const heading = wrapper.content.children[0] as UIDLElementNode
617
+ expect(heading.content.semanticType).toBe('h3')
618
+ expect(heading.content.style?.margin).toEqual({ type: 'static', content: '0' })
619
+ }
620
+ )
621
+
622
+ it('a div that becomes the link keeps its id, its data attributes and its scene lanes on itself', () => {
623
+ const card = styledCard()
624
+ card.content.attrs = {
625
+ id: { type: 'static', content: 'featured-card' },
626
+ 'data-analytics': { type: 'static', content: 'card' },
627
+ 'data-scroll-bind': { type: 'static', content: 'rise-in' },
628
+ }
629
+
630
+ const anchor = insertLinks(withDisplay('grid', card), {}, false).content
631
+ .children[0] as UIDLElementNode
632
+
633
+ expect(anchor.content.elementType).toBe('link')
634
+ for (const kept of ['id', 'data-analytics', 'data-scroll-bind', 'url']) {
635
+ expect(anchor.content.attrs[kept]).toBeDefined()
636
+ }
637
+ })
638
+
639
+ it('a div stays wrapped when it listens to events, carries an attribute an anchor may not, or its parent is not known', () => {
640
+ const clickable = styledCard()
641
+ clickable.content.events = { click: [] }
642
+ const named = styledCard()
643
+ named.content.attrs = { name: { type: 'static', content: 'card' } }
644
+
645
+ for (const node of [clickable, named]) {
646
+ const wrapper = insertLinks(withDisplay('flex', node), {}, false).content
647
+ .children[0] as UIDLElementNode
648
+ expect(wrapper.content.elementType).toBe('link')
649
+ expect((wrapper.content.children[0] as UIDLElementNode).content.elementType).toBe('container')
650
+ }
651
+ const rootWrapper = insertLinks(styledCard(), {}, false)
652
+ expect((rootWrapper.content.children[0] as UIDLElementNode).content.elementType).toBe(
653
+ 'container'
654
+ )
655
+ })
656
+
657
+ it('the wrapper keeps its own box inside a block parent', () => {
658
+ const result = insertLinks(withDisplay('block', styledCard()), {}, false)
659
+ const wrapper = result.content.children[0] as UIDLElementNode
660
+
661
+ expect(wrapper.content.elementType).toBe('link')
662
+ expect(wrapper.content.style?.display).toBeUndefined()
663
+ // a wrapper with a box draws its own focus ring and is not marked
664
+ expect(wrapper.content.attrs['data-thq-link-wrapper']).toBeUndefined()
665
+ })
666
+
667
+ it('a linked root node is wrapped without a box: its parent is unknown here', () => {
668
+ const wrapper = insertLinks(styledCard(), {}, false)
669
+
670
+ expect(wrapper.content.elementType).toBe('link')
671
+ expect(wrapper.content.style?.display).toEqual({ type: 'static', content: 'contents' })
672
+ })
673
+
674
+ it('moves the attributes an anchor may carry onto the wrapper and leaves the rest on the element', () => {
675
+ const card = styledCard()
676
+ card.content.attrs = {
677
+ id: { type: 'static', content: 'featured-card' },
678
+ class: { type: 'static', content: 'promo' },
679
+ title: { type: 'static', content: 'Featured' },
680
+ 'data-analytics': { type: 'static', content: 'card' },
681
+ 'aria-current': { type: 'static', content: 'page' },
682
+ role: { type: 'static', content: 'group' },
683
+ name: { type: 'static', content: 'card' },
684
+ }
685
+
686
+ const wrapper = insertLinks(withDisplay('block', card), {}, false).content
687
+ .children[0] as UIDLElementNode
688
+ const element = wrapper.content.children[0] as UIDLElementNode
689
+
690
+ for (const moved of ['id', 'class', 'title', 'data-analytics', 'aria-current']) {
691
+ expect(wrapper.content.attrs[moved]).toBeDefined()
692
+ expect(element.content.attrs[moved]).toBeUndefined()
693
+ }
694
+ for (const kept of ['role', 'name']) {
695
+ expect(wrapper.content.attrs[kept]).toBeUndefined()
696
+ expect(element.content.attrs[kept]).toBeDefined()
697
+ }
698
+ })
699
+
700
+ it('never turns an element with a meaning of its own into the anchor: it is wrapped and keeps its tag', () => {
701
+ const heading = elementNode('text')
702
+ heading.content.semanticType = 'h2'
703
+ const paragraph = elementNode('text')
704
+ paragraph.content.semanticType = 'p'
705
+ const image = elementNode('image', { src: { type: 'static', content: '/a.jpg' } })
706
+ const listItem = elementNode('container')
707
+ listItem.content.semanticType = 'li'
708
+ const section = elementNode('container')
709
+ section.content.semanticType = 'section'
710
+
711
+ for (const node of [heading, paragraph, image, listItem, section]) {
712
+ const { elementType, semanticType } = node.content
713
+ const wrapper = insertLinks(withDisplay('block', linked(node)), {}, false).content
714
+ .children[0] as UIDLElementNode
715
+ const element = wrapper.content.children[0] as UIDLElementNode
716
+
717
+ expect(wrapper.content.elementType).toBe('link')
718
+ expect(element.content.elementType).toBe(elementType)
719
+ expect(element.content.semanticType).toBe(semanticType)
720
+ }
721
+ })
722
+
723
+ it('a button and a span become the anchor themselves and keep their own styles', () => {
724
+ const button = elementNode('button')
725
+ button.content.style = { padding: { type: 'static', content: '12px' } }
726
+ button.content.attrs = { type: { type: 'static', content: 'button' } }
727
+ const span = elementNode('text')
728
+ span.content.semanticType = 'span'
729
+ span.content.style = { color: { type: 'static', content: 'red' } }
730
+
731
+ const asAnchor = insertLinks(linked(button), {}, false)
732
+ expect(asAnchor.content.elementType).toBe('link')
733
+ expect(asAnchor.content.children ?? []).toHaveLength(0)
734
+ expect(asAnchor.content.style?.padding).toEqual({ type: 'static', content: '12px' })
735
+ expect(asAnchor.content.style?.textAlign).toEqual({ type: 'static', content: 'center' })
736
+ expect(asAnchor.content.attrs.type).toBeUndefined()
737
+ expect(asAnchor.content.attrs.url).toBeDefined()
738
+
739
+ const textAnchor = insertLinks(linked(span), {}, false)
740
+ expect(textAnchor.content.elementType).toBe('link')
741
+ expect(textAnchor.content.style?.color).toEqual({ type: 'static', content: 'red' })
742
+ expect(textAnchor.content.style?.display).toBeUndefined()
743
+ })
744
+
745
+ it('keeps what a motion runtime reads on the element that draws the box, never on the wrapper', () => {
746
+ const card = linkedHeading()
747
+ card.content.attrs = {
748
+ 'data-scene-backdrop': { type: 'static', content: 'true' },
749
+ 'data-scene-track': { type: 'static', content: 'true' },
750
+ 'data-motion-preset': { type: 'static', content: 'slide-up' },
751
+ 'data-tq-motion': { type: 'static', content: 'true' },
752
+ 'data-scroll-video': { type: 'static', content: 'true' },
753
+ 'data-analytics': { type: 'static', content: 'card' },
754
+ }
755
+
756
+ const wrapper = insertLinks(withDisplay('flex', card), {}, false).content
757
+ .children[0] as UIDLElementNode
758
+ const element = wrapper.content.children[0] as UIDLElementNode
759
+
760
+ for (const kept of [
761
+ 'data-scene-backdrop',
762
+ 'data-scene-track',
763
+ 'data-motion-preset',
764
+ 'data-tq-motion',
765
+ 'data-scroll-video',
766
+ ]) {
767
+ expect(element.content.attrs[kept]).toBeDefined()
768
+ expect(wrapper.content.attrs[kept]).toBeUndefined()
769
+ }
770
+ expect(wrapper.content.attrs['data-analytics']).toBeDefined()
771
+ })
772
+
773
+ it('leaves a link that sits inside another link alone: anchors cannot nest', () => {
774
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
775
+ const inner = styledCard()
776
+ const outer = linked(elementNode('container', {}, [inner]))
777
+ outer.content.style = { padding: { type: 'static', content: '8px' } }
778
+
779
+ const wrapper = insertLinks(outer, {}, false)
780
+ const outerElement = wrapper.content.children[0] as UIDLElementNode
781
+ const innerElement = outerElement.content.children[0] as UIDLElementNode
782
+
783
+ expect(wrapper.content.elementType).toBe('link')
784
+ expect(innerElement.content.elementType).toBe('container')
785
+ expect(warn).toHaveBeenCalled()
786
+ warn.mockRestore()
787
+ })
788
+ })
789
+
454
790
  describe('insertLink with link-type prop', () => {
455
791
  it('wraps a container element with a link when link ability references a link-type prop', () => {
456
792
  const node = elementNode('container')
@@ -34,6 +34,21 @@ describe('resolveElement', () => {
34
34
  expect(resolvedElement.attrs.dummy.content).toBe('remains here')
35
35
  })
36
36
 
37
+ it('drops editor-only chapter bookkeeping but keeps the runtime lanes', () => {
38
+ const chapter = element('container', {
39
+ 'data-scroll-bind': staticNode('[{"prop":"opacity","at":[0,1],"values":[0,1]}]'),
40
+ 'data-scroll-bind-rel': staticNode('[{"prop":"opacity","at":[0,1],"values":[0,1]}]'),
41
+ 'data-chapter-window': staticNode('0-0.5'),
42
+ 'data-chapter-template': staticNode('finale'),
43
+ 'data-snap-into-view': staticNode('gentle'),
44
+ })
45
+ const resolver = new Resolver()
46
+ resolver.addMapping(mapping)
47
+ const resolved = resolver.resolveElement(chapter)
48
+
49
+ expect(Object.keys(resolved.attrs)).toEqual(['data-scroll-bind', 'data-snap-into-view'])
50
+ })
51
+
37
52
  it('maps a seflClosing tag', () => {
38
53
  const resolver = new Resolver()
39
54
  const imageElement = element('image', {
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  generateUniqueKeys,
12
12
  createNodesLookup,
13
+ reserveProjectStyleNames,
13
14
  resolveChildren,
14
15
  ensureDataSourceUniqueness,
15
16
  mergeMappings,
@@ -29,6 +30,55 @@ import {
29
30
  } from '@teleporthq/teleport-types'
30
31
  import mapping from './mapping.json'
31
32
 
33
+ // A page named "Manifesto" names its root element `manifesto-container`. The
34
+ // project style sheet is global, and another page had authored a
35
+ // `.manifesto-container` two-column grid: the whole Manifesto page became that
36
+ // grid in the generated site (navigation in one column, hero squeezed into the
37
+ // other), while the editor canvas, which does not use generated names, was fine.
38
+ describe('reserveProjectStyleNames', () => {
39
+ const styleSet = {
40
+ 'manifesto-container': { type: 'reusable-project-style-map', content: {} },
41
+ heroTitle: { type: 'reusable-project-style-map', className: 'hero-title', content: {} },
42
+ } as never
43
+
44
+ it('keeps a generated element class off a class of the project style sheet', () => {
45
+ const root = elementNode('container', {}, [elementNode('text')])
46
+ const uidl = component('Manifesto', root)
47
+ const lookup = {}
48
+
49
+ reserveProjectStyleNames(styleSet, lookup)
50
+ createNodesLookup(uidl, lookup)
51
+ generateUniqueKeys(uidl, lookup)
52
+
53
+ expect(root.content.key).toBe('manifesto-container1')
54
+ expect((root.content.children?.[0].content as UIDLElement).key).toBe('manifesto-text')
55
+ })
56
+
57
+ it('reserves the class a style is emitted under, not only its id', () => {
58
+ const title = elementNode('title')
59
+ const uidl = component('Hero', title)
60
+ const lookup = {}
61
+
62
+ reserveProjectStyleNames(styleSet, lookup)
63
+ createNodesLookup(uidl, lookup)
64
+ generateUniqueKeys(uidl, lookup)
65
+
66
+ expect(title.content.key).toBe('hero-title1')
67
+ })
68
+
69
+ it('changes nothing without a project style sheet', () => {
70
+ const root = elementNode('container')
71
+ const uidl = component('Manifesto', root)
72
+ const lookup = {}
73
+
74
+ reserveProjectStyleNames(undefined, lookup)
75
+ createNodesLookup(uidl, lookup)
76
+ generateUniqueKeys(uidl, lookup)
77
+
78
+ expect(root.content.key).toBe('manifesto-container')
79
+ })
80
+ })
81
+
32
82
  describe('generateUniqueKeys', () => {
33
83
  it('adds name and key to node', async () => {
34
84
  const simpleNode = elementNode('container')
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/resolver.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,aAAa,EACb,WAAW,EACX,OAAO,EACP,gBAAgB,EAEjB,MAAM,4BAA4B,CAAA;AAQnC;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,QAAQ;IAC3B,OAAO,CAAC,OAAO,CAMd;gBAEW,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE;IAQlC,UAAU,CAAC,OAAO,EAAE,OAAO;IAI3B,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,gBAA6C;IAkDxF,cAAc,CACnB,OAAO,EAAE,WAAW,EACpB,OAAO,GAAE,gBAA6C;CAYzD"}
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/resolver.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,aAAa,EACb,WAAW,EACX,OAAO,EACP,gBAAgB,EAEjB,MAAM,4BAA4B,CAAA;AAQnC;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,QAAQ;IAC3B,OAAO,CAAC,OAAO,CAMd;gBAEW,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE;IAQlC,UAAU,CAAC,OAAO,EAAE,OAAO;IAI3B,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,gBAA6C;IAmDxF,cAAc,CACnB,OAAO,EAAE,WAAW,EACpB,OAAO,GAAE,gBAA6C;CAYzD"}
@@ -67,6 +67,7 @@ var Resolver = /** @class */ (function () {
67
67
  this.mapping = utils.mergeMappings(this.mapping, mapping);
68
68
  };
69
69
  Resolver.prototype.resolveUIDL = function (input, options) {
70
+ var _a;
70
71
  if (options === void 0) { options = { extractedResources: {} }; }
71
72
  var mapping = utils.mergeMappings(this.mapping, options.mapping);
72
73
  var newOptions = __assign(__assign({}, options), { mapping: mapping });
@@ -95,6 +96,7 @@ var Resolver = /** @class */ (function () {
95
96
  // never generated). Left untouched they compile to `ReferenceError`s that
96
97
  // fail the static export / `next build`.
97
98
  (0, unbound_expressions_1.resolveUnboundExpressions)(uidl);
99
+ utils.reserveProjectStyleNames((_a = newOptions.projectStyleSet) === null || _a === void 0 ? void 0 : _a.styleSetDefinitions, lookup);
98
100
  utils.createNodesLookup(uidl, lookup);
99
101
  utils.generateUniqueKeys(uidl, lookup);
100
102
  utils.ensureDataSourceUniqueness(uidl.node);
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../src/resolver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../src/resolver.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  import { GeneratorOptions, UIDLLinkNode, UIDLElementNode, UIDLPropDefinition } from '@teleporthq/teleport-types';
2
+ export declare const isRuntimeDataAttribute: (attrName: string) => boolean;
3
+ export declare const BOXLESS_LINK_WRAPPER_ATTR = "data-thq-link-wrapper";
2
4
  export declare const insertLinks: (node: UIDLElementNode, options: GeneratorOptions, linkInParent?: boolean, parentNode?: UIDLElementNode, propDefinitions?: Record<string, UIDLPropDefinition>) => UIDLElementNode;
3
5
  export declare const createLinkNode: (link: UIDLLinkNode, options: GeneratorOptions) => UIDLElementNode;
4
6
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/resolvers/abilities/utils.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,YAAY,EAEZ,eAAe,EAEf,kBAAkB,EAEnB,MAAM,4BAA4B,CAAA;AAwDnC,eAAO,MAAM,WAAW,SAChB,eAAe,WACZ,gBAAgB,iBACX,OAAO,eACR,eAAe,oBACV,OAAO,MAAM,EAAE,kBAAkB,CAAC,KACnD,eAoUF,CAAA;AA6FD,eAAO,MAAM,cAAc,SAAU,YAAY,WAAW,gBAAgB,KAAG,eAS9E,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/resolvers/abilities/utils.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,YAAY,EAEZ,eAAe,EAEf,kBAAkB,EAInB,MAAM,4BAA4B,CAAA;AAkDnC,eAAO,MAAM,sBAAsB,aAAc,MAAM,KAAG,OACqB,CAAA;AAqE/E,eAAO,MAAM,yBAAyB,0BAA0B,CAAA;AAsChE,eAAO,MAAM,WAAW,SAChB,eAAe,WACZ,gBAAgB,iBACX,OAAO,eACR,eAAe,oBACV,OAAO,MAAM,EAAE,kBAAkB,CAAC,KACnD,eA+VF,CAAA;AA0FD,eAAO,MAAM,cAAc,SAAU,YAAY,WAAW,gBAAgB,KAAG,eAS9E,CAAA"}