@@ -26,6 +26,15 @@ public function testPreLex(string $input, string $expectedOutput): void
26
26
$ this ->assertSame ($ expectedOutput , $ lexer ->preLexComponents ($ input ));
27
27
}
28
28
29
+ /**
30
+ * @dataProvider getLexTestsWhithShortOptions
31
+ */
32
+ public function testPreLexWithShortTags (string $ input , string $ expectedOutput ): void
33
+ {
34
+ $ lexer = new TwigPreLexer (withShortTags: true );
35
+ $ this ->assertSame ($ expectedOutput , $ lexer ->preLexComponents ($ input ));
36
+ }
37
+
29
38
/**
30
39
* @dataProvider getInvalidSyntaxTests
31
40
*/
@@ -376,6 +385,12 @@ public static function getLexTests(): iterable
376
385
'<twig:foobar bar="baz" {{ ...attr }}>content</twig:foobar> ' ,
377
386
'{% component \'foobar \' with { bar: \'baz \', ...attr } %}{% block content %}content{% endblock %}{% endcomponent %} ' ,
378
387
];
388
+
389
+ yield 'jsx_component_simple_component_not_enabled_by_default ' => [
390
+ '<Foo /> ' ,
391
+ '<Foo /> ' ,
392
+ ];
393
+
379
394
yield 'component_with_comment_line ' => [
380
395
"<twig:foo \n # bar \n /> " ,
381
396
'{{ component( \'foo \') }} ' ,
@@ -437,4 +452,271 @@ public static function getLexTests(): iterable
437
452
TWIG ,
438
453
];
439
454
}
455
+
456
+ public static function getLexTestsWhithShortOptions ()
457
+ {
458
+ yield 'not_a_component ' => [
459
+ '<foo /> ' ,
460
+ '<foo /> ' ,
461
+ ];
462
+
463
+ yield 'jsx_component_simple_component ' => [
464
+ '<Foo /> ' ,
465
+ '{{ component( \'foo \') }} ' ,
466
+ ];
467
+
468
+ yield 'jsx_component_attribute_with_no_value_and_no_attributes ' => [
469
+ '<Foo/> ' ,
470
+ '{{ component( \'foo \') }} ' ,
471
+ ];
472
+
473
+ yield 'jsx_component_with_default_block_content ' => [
474
+ '<Foo>Foo</Foo> ' ,
475
+ '{% component \'foo \' %}{% block content %}Foo{% endblock %}{% endcomponent %} ' ,
476
+ ];
477
+
478
+ yield 'jsx_component_with_default_block_that_holds_a_component_and_multi_blocks ' => [
479
+ '<Foo>Foo <twig:bar /><twig:block name="other_block">Other block</twig:block></Foo> ' ,
480
+ '{% component \'foo \' %}{% block content %}Foo {{ component( \'bar \') }}{% endblock %}{% block other_block %}Other block{% endblock %}{% endcomponent %} ' ,
481
+ ];
482
+
483
+ yield 'jsx_component_with_character_:_on_his_name ' => [
484
+ '<Foo:bar></Foo:bar> ' ,
485
+ '{% component \'foo:bar \' %}{% endcomponent %} ' ,
486
+ ];
487
+
488
+ yield 'jsx_component_with_character_-_on_his_name ' => [
489
+ '<Foo-bar></Foo-bar> ' ,
490
+ '{% component \'foo-bar \' %}{% endcomponent %} ' ,
491
+ ];
492
+
493
+ yield 'jsx_component_with_character_._on_his_name ' => [
494
+ '<Foo.bar></Foo.bar> ' ,
495
+ '{% component \'foo.bar \' %}{% endcomponent %} ' ,
496
+ ];
497
+
498
+ yield 'jsx_component_with_block ' => [
499
+ '<SuccessAlert>
500
+ <Block name="alert_message">
501
+ xxxx
502
+ </Block>
503
+ </SuccessAlert> ' ,
504
+ '{% component \'successAlert \' %}
505
+ {% block alert_message %}
506
+ xxxx
507
+ {% endblock %}
508
+ {% endcomponent %} ' ,
509
+ ];
510
+
511
+ yield 'jsx_component_with_sub_blocks ' => [
512
+ '<SuccessAlert>
513
+ <Message name="alert_message">
514
+ <Icon name="success" />
515
+ </Message>
516
+ <Message name="alert_message">
517
+ <Icon name="success" />
518
+ </Message>
519
+ </SuccessAlert> ' ,
520
+ '{% component \'successAlert \' %}
521
+ {% block content %}{% component \'message \' with { name: \'alert_message \' } %}
522
+ {% block content %}{{ component( \'icon \', { name: \'success \' }) }}
523
+ {% endblock %}{% endcomponent %}
524
+ {% component \'message \' with { name: \'alert_message \' } %}
525
+ {% block content %}{{ component( \'icon \', { name: \'success \' }) }}
526
+ {% endblock %}{% endcomponent %}
527
+ {% endblock %}{% endcomponent %} ' ,
528
+ ];
529
+
530
+ yield 'jsx_component_with_multiple_nested_namespaces ' => [
531
+ '<Foo:Bar:Baz></Foo:Bar:Baz> ' ,
532
+ '{% component \'foo:Bar:Baz \' %}{% endcomponent %} ' ,
533
+ ];
534
+
535
+ yield 'mixing_standard_and_jsx_components ' => [
536
+ '<Alert><twig:Button>Click me</twig:Button></Alert> ' ,
537
+ "{% component 'alert' %}{% block content %}{% component 'Button' %}{% block content %}Click me{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %} " ,
538
+ ];
539
+
540
+ yield 'jsx_component_with_dynamic_attributes ' => [
541
+ '<Alert :level="alertLevel" title="{{ title }}"></Alert> ' ,
542
+ '{% component \'alert \' with { level: alertLevel, title: (title) } %}{% endcomponent %} ' ,
543
+ ];
544
+
545
+ yield 'jsx_component_with_spreading ' => [
546
+ '<Button {{ ...buttonAttrs }}>Click me</Button> ' ,
547
+ '{% component \'button \' with { ...buttonAttrs } %}{% block content %}Click me{% endblock %}{% endcomponent %} ' ,
548
+ ];
549
+
550
+ yield 'jsx_component_with_named_blocks ' => [
551
+ '<Card><Block name="header">Title</Block><Block name="body">Content</Block></Card> ' ,
552
+ '{% component \'card \' %}{% block header %}Title{% endblock %}{% block body %}Content{% endblock %}{% endcomponent %} ' ,
553
+ ];
554
+
555
+ yield 'nested_jsx_components_with_namespaces ' => [
556
+ '<UI:Layout><UI:Sidebar>Menu</UI:Sidebar><UI:Content>Page</UI:Content></UI:Layout> ' ,
557
+ "{% component 'uI:Layout' %}{% block content %}{% component 'uI:Sidebar' %}{% block content %}Menu{% endblock %}{% endcomponent %}{% component 'uI:Content' %}{% block content %}Page{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %} " ,
558
+ ];
559
+
560
+ yield 'normal_html_tags_not_transformed ' => [
561
+ '<div><span>Text</span><input type="text" /></div> ' ,
562
+ '<div><span>Text</span><input type="text" /></div> ' ,
563
+ ];
564
+
565
+ yield 'lowercase_component_name_not_transformed ' => [
566
+ '<foo>Content</foo> ' ,
567
+ '<foo>Content</foo> ' ,
568
+ ];
569
+
570
+ yield 'jsx_component_with_special_characters_in_attributes ' => [
571
+ '<Button data-testid="test-btn" aria-label="Click me"></Button> ' ,
572
+ '{% component \'button \' with { \'data-testid \': \'test-btn \', \'aria-label \': \'Click me \' } %}{% endcomponent %} ' ,
573
+ ];
574
+
575
+ yield 'jsx_component_with_html_comments ' => [
576
+ '<Alert><!-- This is a comment --><Block name="title">Alert title</Block></Alert> ' ,
577
+ '{% component \'alert \' %}{% block content %}<!-- This is a comment -->{% endblock %}{% block title %}Alert title{% endblock %}{% endcomponent %} ' ,
578
+ ];
579
+
580
+ yield 'jsx_component_with_boolean_attributes ' => [
581
+ '<Button disabled primary>Click me</Button> ' ,
582
+ '{% component \'button \' with { disabled: true, primary: true } %}{% block content %}Click me{% endblock %}{% endcomponent %} ' ,
583
+ ];
584
+
585
+ yield 'jsx_component_with_whitespace ' => [
586
+ '<Button
587
+ type="primary"
588
+ size="large"
589
+ >
590
+ Submit
591
+ </Button> ' ,
592
+ '{% component \'button \' with { type: \'primary \', size: \'large \' } %}
593
+ {% block content %}Submit
594
+ {% endblock %}{% endcomponent %} ' ,
595
+ ];
596
+
597
+ yield 'jsx_component_with_numbers_in_name ' => [
598
+ '<Grid3x3>Content</Grid3x3> ' ,
599
+ '{% component \'grid3x3 \' %}{% block content %}Content{% endblock %}{% endcomponent %} ' ,
600
+ ];
601
+
602
+ yield 'jsx_component_with_complex_expressions ' => [
603
+ '<DataTable :items="items|filter(item => item.active)|sort((a, b) => a.name <=> b.name)" /> ' ,
604
+ '{{ component( \'dataTable \', { items: items|filter(item => item.active)|sort((a, b) => a.name <=> b.name) }) }} ' ,
605
+ ];
606
+
607
+ // Looks like HTML 5 custom elements
608
+ yield 'jsx_component_similar_to_custom_element ' => [
609
+ '<Custom-Element data-value="test">Content</Custom-Element> ' ,
610
+ "{% component 'custom-Element' with { 'data-value': 'test' } %}{% block content %}Content{% endblock %}{% endcomponent %} " ,
611
+ ];
612
+
613
+ yield 'nested_jsx_components_with_same_name ' => [
614
+ '<Section><Section>Nested</Section></Section> ' ,
615
+ '{% component \'section \' %}{% block content %}{% component \'section \' %}{% block content %}Nested{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %} ' ,
616
+ ];
617
+
618
+ yield 'jsx_component_with_embedded_twig_conditions ' => [
619
+ '<Card>{% if showTitle %}<Block name="title">Title</Block>{% endif %}</Card> ' ,
620
+ "{% component 'card' %}{% block content %}{% if showTitle %}{% endblock %}{% block title %}Title{% endblock %}{% block content %}{% endif %}{% endblock %}{% endcomponent %} " ,
621
+ ];
622
+
623
+ yield 'jsx_component_with_embedded_twig_loops ' => [
624
+ '<List>{% for item in items %}<Item :value="item">{{ item.name }}</Item>{% endfor %}</List> ' ,
625
+ '{% component \'list \' %}{% block content %}{% for item in items %}{% component \'item \' with { value: item } %}{% block content %}{{ item.name }}{% endblock %}{% endcomponent %}{% endfor %}{% endblock %}{% endcomponent %} ' ,
626
+ ];
627
+
628
+ yield 'jsx_component_with_escaped_attribute_values ' => [
629
+ '<Alert message="This is a \'quoted \' message" /> ' ,
630
+ "{{ component('alert', { message: 'This is a \'quoted\' message' }) }} " ,
631
+ ];
632
+
633
+ yield 'jsx_component_with_self_closing_html_in_content ' => [
634
+ '<Card><img src="image.jpg" /><hr/></Card> ' ,
635
+ '{% component \'card \' %}{% block content %}<img src="image.jpg" /><hr/>{% endblock %}{% endcomponent %} ' ,
636
+ ];
637
+
638
+ yield 'jsx_component_with_array_and_object_attributes ' => [
639
+ '<Select :options="[ \'option1 \', \'option2 \']" :config="{ multiselect: true }" /> ' ,
640
+ '{{ component( \'select \', { options: [ \'option1 \', \'option2 \'], config: { multiselect: true } }) }} ' ,
641
+ ];
642
+
643
+ yield 'jsx_component_interpolation_inside_dynamic_attribute ' => [
644
+ '<Button :class="isActive ? \'active-{{ theme }} \' : \'inactive \'" /> ' ,
645
+ "{{ component('button', { class: isActive ? 'active-{{ theme }}' : 'inactive' }) }} " ,
646
+ ];
647
+
648
+ yield 'jsx_component_with_mixed_case_name ' => [
649
+ '<DataTable sorting="asc">Content</DataTable> ' ,
650
+ '{% component \'dataTable \' with { sorting: \'asc \' } %}{% block content %}Content{% endblock %}{% endcomponent %} ' ,
651
+ ];
652
+
653
+ yield 'jsx_component_with_namespace_and_mixed_case ' => [
654
+ '<App:UserProfile:Avatar size="medium" /> ' ,
655
+ '{{ component( \'app:UserProfile:Avatar \', { size: \'medium \' }) }} ' ,
656
+ ];
657
+
658
+ yield 'jsx_component_with_complex_twig_in_attributes ' => [
659
+ '<Form :errors="form.errors is defined ? form.errors : {}" :disabled="form.isSubmitting ?? false" /> ' ,
660
+ '{{ component( \'form \', { errors: form.errors is defined ? form.errors : {}, disabled: form.isSubmitting ?? false }) }} ' ,
661
+ ];
662
+
663
+ yield 'jsx_component_with_path_expression_in_attributes ' => [
664
+ '<Field :value="user.address.street" :error="errors.address.street|default(null)" /> ' ,
665
+ '{{ component( \'field \', { value: user.address.street, error: errors.address.street|default(null) }) }} ' ,
666
+ ];
667
+
668
+ yield 'nested_jsx_components_with_complex_blocks ' => [
669
+ '<Tabs>
670
+ <Tab title="First">
671
+ <Panel>
672
+ <Block name="header">Title</Block>
673
+ <Block name="body">Content</Block>
674
+ </Panel>
675
+ </Tab>
676
+ </Tabs> ' ,
677
+ "{% component 'tabs' %}
678
+ {% block content %}{% component 'tab' with { title: 'First' } %}
679
+ {% block content %}{% component 'panel' %}
680
+ {% block header %}Title{% endblock %}
681
+ {% block body %}Content{% endblock %}
682
+ {% endcomponent %}
683
+ {% endblock %}{% endcomponent %}
684
+ {% endblock %}{% endcomponent %} " ,
685
+ ];
686
+
687
+ yield 'jsx_component_with_aria_and_data_attributes ' => [
688
+ '<Button aria-pressed="false" data-analytics-id="login-btn">Login</Button> ' ,
689
+ '{% component \'button \' with { \'aria-pressed \': \'false \', \'data-analytics-id \': \'login-btn \' } %}{% block content %}Login{% endblock %}{% endcomponent %} ' ,
690
+ ];
691
+
692
+ yield 'jsx_component_with_twig_filters ' => [
693
+ '<Alert :message="error|trans|capitalize" /> ' ,
694
+ '{{ component( \'alert \', { message: error|trans|capitalize }) }} ' ,
695
+ ];
696
+
697
+ yield 'jsx_component_with_twig_macros ' => [
698
+ '<Card>{% import "macros.twig" as forms %}<Block name="body">{{ forms.input("username") }}</Block></Card> ' ,
699
+ '{% component \'card \' %}{% block content %}{% import "macros.twig" as forms %}{% endblock %}{% block body %}{{ forms.input("username") }}{% endblock %}{% endcomponent %} ' ,
700
+ ];
701
+
702
+ yield 'jsx_component_with_mixed_content ' => [
703
+ '<Notice>This is <strong>important</strong> and <em>urgent</em>.</Notice> ' ,
704
+ '{% component \'notice \' %}{% block content %}This is <strong>important</strong> and <em>urgent</em>.{% endblock %}{% endcomponent %} ' ,
705
+ ];
706
+
707
+ yield 'jsx_component_with_short_namespace ' => [
708
+ '<X:Y /> ' ,
709
+ '{{ component( \'x:Y \') }} ' ,
710
+ ];
711
+
712
+ yield 'jsx_component_with_namespaced_attributes ' => [
713
+ '<Svg xmlns:xlink="http://www.w3.org/1999/xlink" /> ' ,
714
+ '{{ component( \'svg \', { \'xmlns:xlink \': \'http://www.w3.org/1999/xlink \' }) }} ' ,
715
+ ];
716
+
717
+ yield 'jsx_component_with_block_expression ' => [
718
+ '<Card><Block name="{{ showHeader ? \'header \' : \'footer \' }}">Content</Block></Card> ' ,
719
+ "{% component 'card' %}{% block (showHeader ? 'header' %}Content{% endblock %}{% endcomponent %} " ,
720
+ ];
721
+ }
440
722
}
0 commit comments