diff --git a/UnitTestContrib/test/unit/ExtensionsTests.pm b/UnitTestContrib/test/unit/ExtensionsTests.pm index 8333e2ad9b..94a8bacbad 100644 --- a/UnitTestContrib/test/unit/ExtensionsTests.pm +++ b/UnitTestContrib/test/unit/ExtensionsTests.pm @@ -425,6 +425,101 @@ CFG_EXT ); } +sub test_tag_handlers { + my $this = shift; + + # Testing three macros registered by the same extension using three + # different approaches: + # 1. A macro to be handled by an extension method with the same name as + # macro's. + # 2. Similar to above but method code is declared as tagHandler's second + # parameter. + # 3. A macro class with Foswiki::Macro role. + # All three are refering the same extension object and it's autoincementing + # counter attribute. But as long as the first two approaches are extension's + # methods and rely upon valid $this parameter the class is obtaining + # extension's object by requesting application's extensions attribute. + + my ($ext) = $this->_genExtModules( 1, <<'TAGH'); + +has counter => ( + is => 'rw', + lazy => 1, + default => 0, +); + +around counter => sub { + my $orig = shift; + my $this = shift; + my $val = $orig->($this); + $orig->( $this, $val + 1 ); + return sprintf( '%03d', $val ); +}; + +tagHandler 'TEST_EXT_MACRO1'; + +tagHandler TEST_EXT_MACRO2 => sub { + my $this = shift; + + return 'TEST_EXT_MACRO2_' . $this->counter; +}; + +tagHandler TEST_CLASS_MACRO => 'Foswiki::Macros::TEST_CLASS_MACRO'; + +sub TEST_EXT_MACRO1 { + my $this = shift; + return "TEST_EXT_MACRO1_" . $this->counter; +} +TAGH + + my $macroPackage = <app->extensions->extObject('$ext'); + + return "TEST_CLASS_MACRO_" . \$myExt->counter; + } +1; +MPKG + + if ( !eval($macroPackage) || $@ ) { + Foswiki::Exception::Fatal->throw( + text => "Failed to compile macro class: " . $@ ); + } + + $this->reCreateFoswikiApp; + + $this->test_topicObject->text('%TEST_EXT_MACRO1%'); + + $this->assert_str_equals( 'TEST_EXT_MACRO1_000', + $this->test_topicObject->expandMacros('%TEST_EXT_MACRO1%') ); + $this->assert_str_equals( 'TEST_EXT_MACRO1_001', + $this->test_topicObject->expandMacros('%TEST_EXT_MACRO1%') ); + $this->assert_str_equals( 'TEST_CLASS_MACRO_002', + $this->test_topicObject->expandMacros('%TEST_CLASS_MACRO%') ); + $this->assert_str_equals( 'TEST_EXT_MACRO2_003', + $this->test_topicObject->expandMacros('%TEST_EXT_MACRO2%') ); +} + +sub test_extName_method { + my $this = shift; + + my ($ext) = $this->_genExtModules( 1, <<'SNEXT'); +our $NAME = "AutoGenExt"; +SNEXT + + $this->reCreateFoswikiApp; + + $this->assert_str_equals( 'AutoGenExt', + $this->app->extensions->extName($ext) ); +} + 1; __END__ Foswiki - The Free and Open Source Wiki, http://foswiki.org/ diff --git a/core/lib/Foswiki/App.pm b/core/lib/Foswiki/App.pm index 9d525652d5..1626bb0a52 100644 --- a/core/lib/Foswiki/App.pm +++ b/core/lib/Foswiki/App.pm @@ -402,6 +402,7 @@ sub BUILD { } else { my $plogin = $this->plugins->load; + $this->extensions->initialize; $this->engine->user($plogin) if $plogin; } @@ -1192,6 +1193,7 @@ sub systemMessage { my $this = shift; if (@_) { push @{ $this->system_messages }, @_; + return; } # SMELL Something better than %BR% shall be used here. diff --git a/core/lib/Foswiki/Class.pm b/core/lib/Foswiki/Class.pm index 19ba3e1035..f972aba1bc 100644 --- a/core/lib/Foswiki/Class.pm +++ b/core/lib/Foswiki/Class.pm @@ -239,6 +239,26 @@ sub _handler_extBefore (@) { Foswiki::Extensions::registerDeps( $_, $target ) foreach @_; } +sub _handler_tagHandler ($;$) { + my $target = caller; + + # Handler could be a class name doing Foswiki::Macro role or a sub to be + # installed as target's hadnling method. + my ( $tagName, $tagHandler ) = @_; + + if ( ref($tagHandler) eq 'CODE' ) { + + # If second argument is a code ref then we install method with the same + # name as macro name. + _inject_code( $target, $tagName, $tagHandler ); + Foswiki::Extensions::registerExtTagHandler( $target, $tagName ); + } + else { + Foswiki::Extensions::registerExtTagHandler( $target, $tagName, + $tagHandler ); + } +} + sub _install_extension { my ( $class, $target ) = @_; @@ -248,6 +268,7 @@ sub _install_extension { _inject_code( $target, 'extClass', \&_handler_extClass ); _inject_code( $target, 'extAfter', \&_handler_extAfter ); _inject_code( $target, 'extBefore', \&_handler_extBefore ); + _inject_code( $target, 'tagHandler', \&_handler_tagHandler ); } sub _handler_pluggable ($&) { diff --git a/core/lib/Foswiki/Extensions.pm b/core/lib/Foswiki/Extensions.pm index 1fc6e0e3d2..0f159617a6 100644 --- a/core/lib/Foswiki/Extensions.pm +++ b/core/lib/Foswiki/Extensions.pm @@ -39,6 +39,7 @@ our @extModules our %registeredModules; # Modules registered with registerExtModule(). our %extSubClasses; # Subclasses registered by extensions. our %extDeps; # Module dependecies. Influences the order of extension objects. +our %extTags; # Tags registered by extensions. our %pluggables; # Pluggable methods our %plugMethods; # Extension registered plug methods. @@ -142,8 +143,6 @@ sub BUILD { my $this = shift; $this->loadExtensions; - - $this->initializeExtensions; } sub normalizeExtName { @@ -159,17 +158,29 @@ sub normalizeExtName { sub extEnabled { my $this = shift; - my ($extName) = @_; + my ($ext) = @_; - $extName = $this->normalizeExtName($extName); + my $extName = $this->normalizeExtName($ext); return defined $this->disabledExtensions->{$extName} ? undef : $extName; } +sub extObject { + my $this = shift; + my ($ext) = @_; + + my $extName = $this->normalizeExtName($ext); + + return $this->extensions->{$extName}; +} + sub isBadVersion { my $this = shift; my ($extName) = @_; + return "Extension module $extName not a subclass of Foswiki::Extension" + unless $extName->isa('Foswiki::Extension'); + my @apiScalar = grep { /::API_VERSION$/ } Devel::Symdump->scalars($extName); return "No \$API_VERSION scalar defined in $extName" @@ -267,8 +278,16 @@ sub loadExtensions { } } -sub initializeExtensions { +sub initialize { my $this = shift; + + # Register macro tag handlers for enabled extensions. + foreach my $tag ( keys %extTags ) { + if ( $this->extEnabled( $extTags{$tag}{extension} ) ) { + my $handler = $extTags{$tag}{class} // $extTags{$tag}{extension}; + $this->app->macros->registerTagHandler( $tag, $handler ); + } + } } sub _extVisit { @@ -483,7 +502,7 @@ sub prepareDisabledExtensions { Foswiki::Exception::Fatal->throw( text => "Environment variable $envVar is a ref to " . $reftype - . " but ARRAY excepted" ) + . " but ARRAY or scalar string expected" ) unless $reftype eq 'ARRAY'; } else { @@ -760,6 +779,20 @@ sub _callPluggable { return $origCode->( $params{object}, @{ $params{args} } ); } +# Universal methods supporting static, on class, and object calls. +sub extName { + shift if ref( $_[0] ) && $_[0]->isa('Foswiki::Extensions'); + my ($extName) = @_; + + my $name = Foswiki::fetchGlobal( "\$" . $extName . "::NAME" ); + + unless ($name) { + ( $name = $extName ) =~ s/^Foswiki::Extension:://; + } + + return $name // ''; +} + =begin TML ---++ Static methods @@ -783,6 +816,15 @@ sub registerExtModule { $registeredModules{$extModule} = 1; } +sub registerExtTagHandler { + my ( $extModule, $tagName, $tagClass ) = @_; + + $extTags{$tagName} = { + extension => $extModule, + ( defined $tagClass ? ( class => $tagClass ) : () ), + }; +} + sub registerDeps { my $extModule = shift; diff --git a/core/lib/Foswiki/Macro.pm b/core/lib/Foswiki/Macro.pm index 752ce8753e..880acc4088 100644 --- a/core/lib/Foswiki/Macro.pm +++ b/core/lib/Foswiki/Macro.pm @@ -4,7 +4,6 @@ package Foswiki::Macro; use v5.14; use Moo::Role; -use namespace::clean; requires 'expand'; diff --git a/core/lib/Foswiki/Macros.pm b/core/lib/Foswiki/Macros.pm index 8a3e26b726..9920a4725b 100644 --- a/core/lib/Foswiki/Macros.pm +++ b/core/lib/Foswiki/Macros.pm @@ -1,7 +1,6 @@ # See bottom of file for license and copyright information package Foswiki::Macros; -use v5.14; use Foswiki qw(%regex expandStandardEscapes); use Foswiki::Attrs (); @@ -84,9 +83,12 @@ sub registerTagHandler { $this->app->logger->log( 'warning', "Re-registering existing tag " . $tag, ) if exists $this->registered->{$tag}; - Foswiki::Exception::Fatal->throw( - text => "Tag handler object doesn't do Foswiki::Macro role" ) - unless ref($handler) eq 'CODE' || $handler->does('Foswiki::Macro'); + Foswiki::Exception::Fatal->throw( text => +"Invalid tag handler object: must be a code, a Foswiki::Macro, or a Foswiki::Extension" + ) + unless ref($handler) eq 'CODE' + || $handler->does('Foswiki::Macro') + || $handler->isa('Foswiki::Extension'); $this->registered->{$tag} = $handler; if ( $syntax && $syntax eq 'context-free' ) { @@ -663,6 +665,9 @@ sub execMacro { my $this = shift; my ( $macroName, $attrs, $topicObject, @macroArgs ) = @_; + my $app = $this->app; + my $extensions = $app->extensions; + my $rc; # vrurg Macro could either be a reference to an object or a sub. Though @@ -686,22 +691,44 @@ sub execMacro { if ( ref( $this->registered->{$macroName} ) eq 'CODE' ) { $rc = $this->registered->{$macroName} - ->( $this->app, $attrs, $topicObject, @macroArgs ); + ->( $app, $attrs, $topicObject, @macroArgs ); } else { # Create macro object unless it already exists. - unless ( defined $this->_macros->{$macroName} ) { - $this->_macros->{$macroName} = - $this->create( $this->registered->{$macroName} ); - ASSERT( $this->_macros->{$macroName}->does('Foswiki::Macro'), - "Invalid macro module " - . $this->registered->{$macroName} - . "; must do Foswiki::Macro role" ) - if DEBUG; + my $macroObj; + my $macroClass = $this->registered->{$macroName}; + my $methodName = 'expand'; + + if ( UNIVERSAL::isa( $macroClass, 'Foswiki::Extension' ) ) { + if ( $extensions->extEnabled($macroClass) ) { + $macroObj = $this->app->extensions->extObject($macroClass); + $methodName = $macroName; + } + else { + # SMELL Some better way to report macro from a disabled + # extension? Anyway, this must not happen unless an extension + # has been disabled manually after extensions have been + # initialized. + $rc = "$macroName disabled, check extension " + . $extensions->extName($macroClass); + } } - $rc = - $this->_macros->{$macroName} - ->expand( $attrs, $topicObject, @macroArgs ); + else { + if ( defined $this->_macros->{$macroName} ) { + $macroObj = $this->_macros->{$macroName}; + } + else { + $macroObj = $this->_macros->{$macroName} = + $this->create( $this->registered->{$macroName} ); + ASSERT( $this->_macros->{$macroName}->does('Foswiki::Macro'), + "Invalid macro module " + . $this->registered->{$macroName} + . "; must do Foswiki::Macro role" ) + if DEBUG; + } + } + $rc = $macroObj->$methodName( $attrs, $topicObject, @macroArgs ) + if defined $macroObj; } return $rc; diff --git a/core/lib/Foswiki/Meta.pm b/core/lib/Foswiki/Meta.pm index 8180dc1f90..1d2e94fe05 100644 --- a/core/lib/Foswiki/Meta.pm +++ b/core/lib/Foswiki/Meta.pm @@ -1,5 +1,7 @@ # See bottom of file for license and copyright information +package Foswiki::Meta; + =begin TML ---+ package Foswiki::Meta @@ -104,9 +106,6 @@ the function or parameter. =cut -package Foswiki::Meta; -use v5.14; - use Try::Tiny; use Assert; use Errno 'EINTR';