@@ -73,6 +73,9 @@ class NewMail
7373 /** @var array */
7474 private static $ mailer = ['mail ' => 'Mail ' , 'sendmail ' => 'SendMail ' , 'smtp ' => 'SMTP ' ];
7575
76+ /** @var array<string, callable> */
77+ private static $ blockHandlers = [];
78+
7679 /** @var BaseBlock[] */
7780 protected $ mainBlocks = [];
7881
@@ -133,6 +136,16 @@ public function __construct()
133136 $ this ->verificode = Tools::randomString (20 );
134137 }
135138
139+ /**
140+ * Registra un handler personalizado para un tipo de shortcode de bloque.
141+ * El callable recibe (array $attrs, string $content) y debe devolver un BaseBlock o null.
142+ * Ejemplo: NewMail::addBlockHandler('myblock', fn($attrs, $content) => new MyBlock($content));
143+ */
144+ public static function addBlockHandler (string $ tag , callable $ handler ): void
145+ {
146+ self ::$ blockHandlers [strtolower ($ tag )] = $ handler ;
147+ }
148+
136149 public static function addMailer (string $ key , string $ name ): void
137150 {
138151 if (array_key_exists ($ key , self ::$ mailer )) {
@@ -411,6 +424,9 @@ public function send(): bool
411424 }
412425 }
413426
427+ // procesamos los shortcodes de bloque en el texto antes de renderizar
428+ $ this ->replaceTextToBlock ();
429+
414430 $ this ->renderHTML ();
415431 $ this ->mail ->msgHTML ($ this ->html );
416432
@@ -531,6 +547,125 @@ public function to(string $email, string $name = ''): NewMail
531547 return $ this ;
532548 }
533549
550+ /**
551+ * Busca shortcodes de bloque en el texto del email y los convierte en objetos BaseBlock.
552+ * Sintaxis: [blockTipo atributos]contenido[/blockTipo] o [blockTipo atributos] (auto-cierre)
553+ * Ejemplos:
554+ * [blockTitle type="h2"]Bienvenido[/blockTitle]
555+ * [blockText]Párrafo de texto[/blockText]
556+ * [blockHtml]<ul><li>item</li></ul>[/blockHtml]
557+ * [blockButton label="Reservar" href="https://..."]
558+ * [blockSpace height="20"]
559+ * Los plugins pueden registrar tipos adicionales con NewMail::addBlockHandler().
560+ */
561+ protected function replaceTextToBlock (): void
562+ {
563+ if (empty ($ this ->text )) {
564+ return ;
565+ }
566+
567+ // buscamos shortcodes con o sin contenido interior: [blockXxx attrs]...[/blockXxx] o [blockXxx attrs]
568+ preg_match_all (
569+ '/\[block(\w+)([^\]]*)\](?:(.*?)\[\/block\1\])?/s ' ,
570+ $ this ->text ,
571+ $ matches ,
572+ PREG_OFFSET_CAPTURE
573+ );
574+
575+ // si no hay shortcodes, no hacemos nada
576+ if (empty ($ matches [0 ])) {
577+ return ;
578+ }
579+
580+ // guardamos los bloques existentes para añadirlos al final
581+ $ existingBlocks = $ this ->mainBlocks ;
582+ $ this ->mainBlocks = [];
583+ $ text = $ this ->text ;
584+ $ this ->text = '' ;
585+ $ lastPos = 0 ;
586+
587+ foreach ($ matches [0 ] as $ idx => $ match ) {
588+ $ fullMatch = $ match [0 ];
589+ $ offset = $ match [1 ];
590+ $ blockType = $ matches [1 ][$ idx ][0 ];
591+ $ attrsStr = $ matches [2 ][$ idx ][0 ];
592+ $ content = $ matches [3 ][$ idx ][0 ] ?? '' ;
593+
594+ // texto antes del shortcode → TextBlock
595+ $ before = trim (substr ($ text , $ lastPos , $ offset - $ lastPos ));
596+ if ('' !== $ before ) {
597+ $ this ->addMainBlock (new TextBlock ($ before ));
598+ }
599+ $ lastPos = $ offset + strlen ($ fullMatch );
600+
601+ // creamos el bloque correspondiente al shortcode
602+ $ attrs = static ::parseShortcodeAttributes ($ attrsStr );
603+ $ block = $ this ->createBlockFromShortcode ($ blockType , $ attrs , $ content );
604+ if ($ block !== null ) {
605+ $ this ->addMainBlock ($ block );
606+ }
607+ }
608+
609+ // texto restante tras el último shortcode → TextBlock
610+ $ remaining = trim (substr ($ text , $ lastPos ));
611+ if ('' !== $ remaining ) {
612+ $ this ->addMainBlock (new TextBlock ($ remaining ));
613+ }
614+
615+ // reañadimos los bloques que existían antes
616+ foreach ($ existingBlocks as $ block ) {
617+ $ this ->mainBlocks [] = $ block ;
618+ }
619+ }
620+
621+ /**
622+ * Instancia el bloque correspondiente al tipo de shortcode.
623+ * Los plugins pueden ampliar los tipos usando NewMail::addBlockHandler().
624+ */
625+ protected function createBlockFromShortcode (string $ type , array $ attrs , string $ content ): ?BaseBlock
626+ {
627+ // primero comprobamos si hay un handler registrado por un plugin
628+ $ lowerType = strtolower ($ type );
629+ if (isset (self ::$ blockHandlers [$ lowerType ])) {
630+ return (self ::$ blockHandlers [$ lowerType ])($ attrs , $ content );
631+ }
632+
633+ // tipos nativos del core
634+ switch ($ lowerType ) {
635+ case 'title ' :
636+ return new TitleBlock ($ content , $ attrs ['type ' ] ?? 'h2 ' , $ attrs ['css ' ] ?? '' , $ attrs ['style ' ] ?? '' );
637+ case 'text ' :
638+ return new TextBlock ($ content , $ attrs ['css ' ] ?? '' , $ attrs ['style ' ] ?? '' );
639+ case 'html ' :
640+ return new HtmlBlock ($ content );
641+ case 'button ' :
642+ return new ButtonBlock (
643+ $ attrs ['label ' ] ?? $ content ,
644+ $ attrs ['href ' ] ?? '' ,
645+ $ attrs ['css ' ] ?? '' ,
646+ $ attrs ['style ' ] ?? ''
647+ );
648+ case 'space ' :
649+ return new SpaceBlock ((float )($ attrs ['height ' ] ?? 30 ));
650+ }
651+
652+ return null ;
653+ }
654+
655+ /**
656+ * Parsea los atributos de un shortcode en un array clave => valor.
657+ * Soporta comillas simples y dobles: attr="valor" o attr='valor'
658+ */
659+ protected static function parseShortcodeAttributes (string $ attrsStr ): array
660+ {
661+ preg_match_all ('/(\w+)=[" \']([^" \']*)[" \']/ ' , $ attrsStr , $ matches );
662+ $ attrs = [];
663+ foreach ($ matches [1 ] as $ i => $ key ) {
664+ $ attrs [$ key ] = $ matches [2 ][$ i ];
665+ }
666+ return $ attrs ;
667+ }
668+
534669 /**
535670 * Devuelve los bloques del pie del correo.
536671 */
0 commit comments