From 336dfa2214efc54c378cd6916ca86c1ba89f0d57 Mon Sep 17 00:00:00 2001 From: Vadim Belman Date: Sat, 7 Oct 2017 22:09:40 -0400 Subject: [PATCH] Item14237: Documentation corrections and extensions --- EmptyExtension/lib/Foswiki/Extension/Empty.pm | 280 ++++++++++++------ core/data/System/FoswikiV3Essentials.txt | 4 +- core/lib/Foswiki/Class.pm | 4 +- core/lib/Foswiki/ExtManager.pm | 59 ++-- 4 files changed, 227 insertions(+), 120 deletions(-) diff --git a/EmptyExtension/lib/Foswiki/Extension/Empty.pm b/EmptyExtension/lib/Foswiki/Extension/Empty.pm index a924b1c4c3..9ee0fc184c 100644 --- a/EmptyExtension/lib/Foswiki/Extension/Empty.pm +++ b/EmptyExtension/lib/Foswiki/Extension/Empty.pm @@ -9,11 +9,13 @@ package Foswiki::Extension::Empty; This is a template module demostrating basic functionality provided by %WIKITOOLNAME% extensions framework. -__NOTE:__ This document is incomplete for the moment and only focuses on key -details of the new Extensions model. +%X% *NOTE:* This document is incomplete for the moment and only focuses on +details directly influencing a new extension development. =cut +use Foswiki::FeatureSet; + use Foswiki::Class qw(extension); extends qw(Foswiki::Extension); @@ -21,20 +23,21 @@ extends qw(Foswiki::Extension); ---++ The Ecosystem -Extensions exist as a list of objects managed by =Foswiki::App= =extMgr= -attribute which is an instance of =Foswiki::ExtManager= class. The latter -provides API for extension manipulation routines like loading and registering an -extension; registering extension's components; find an extenion object by name; -etc. +Extensions exist as a list of objects managed by +=%PERLDOC{"Foswiki::App" attr="extMgr"}%= attribute which is an instance of +=%PERLDOC{Foswiki::ExtManager}%= class. The latter provides API for extension +manipulation routines like loading and registering an extension; registering +extension's components; find an extenion object by name; etc. An extension should be registered in =Foswiki::Extension::= namespace. I.e. if we create a =Sample= extension then its full name would be =Foswiki::Extension::Sample=. Though this rule is not strictly imposed but it comes in handy when one wants to refer to an extension by its short name. The -manager uses string stored in its =extPrefix= read only attribute to form an -extension's full name; by default the attribute is initialized with -_'Foswiki::Extension'_ string and there is no legal way to change it in a course -of application's life cycle. +manager uses string stored in its +=%PERLDOC{"Foswiki::ExtManager" attr="extPrefix" text="extPrefix"}%= read only +attribute to form an extension's full name; by default the attribute is +initialized with _'Foswiki::Extension'_ string and there is no legal way to +change it in a course of application's life cycle. It is also mandatory for an extension class to subclass =Foswiki::Extension=. The manager would refuse registration if this rule is broken. @@ -44,8 +47,8 @@ At any given moment of time there is only one active set of extensions accessible via an application's =extMgr= attribute. It means that if the =Sample= extension is registered then whenever we query for its object it is guaranteed that there is no more than signle active one exists per application. -This rule is important for some of [[?%QUERYSTRING%#ExportedSubs][exported -subroutines]]. +This rule is important for some of +[[?%QUERYSTRING%#ExportedSubs][exported subroutines]]. =Foswiki::ExtManager= module has its own =$VERSION= global var. It represents %WIKITOOLNAME% API version and is used to check for extension compatibility. @@ -53,9 +56,9 @@ subroutines]]. ---++ Loading Upon startup the extension manager scans a directory (usually it is -_$ENV{FOSWIKI_HOME}/lib/Foswiki/Extension_ but additional subdirs can be defined -by FOSWIKI_EXTLIBS environment variable) for =.pm= files and tries to load them -all in the order as returned by Perl =readdir()= function. +_$ENV{FOSWIKI_HOME}/lib/Foswiki/Extension_ (additional subdirs can be defined - +see %PERLDOC{Foswiki::ExtManager}%) for =.pm= files and tries to load them all +in the order as returned by Perl =readdir()= function. More information could be found in =Foswiki::ExtManager= documentation. @@ -72,16 +75,28 @@ extends qw(Foswiki::Extension); use version 0.77; our $VERSION = version->declare(0.0.1); our $API_VERSION = version->declare("2.99.0"); +our @FS_REQUIRED = qw; -=$API_VERSION= declares the minimal version of =Foswiki::ExtManager= module -required. +Even though =$API_VERSION= in the example won't be used becuase of +=@FS_REQUIRED= (see =%PERLDOC{"Foswiki::ExtManager" section="API +Compatibility"}%=), but it would be courteous to provide it for the code +readers. =cut use version 0.77; our $VERSION = version->declare(0.0.1); our $API_VERSION = version->declare("2.99.0"); -our $NAME = "Empty"; +our $NAME = "Empty"; + +features_provided + -namespace => "Ext::Empty", + EXAMPLE_FEATURE => [ + 2.99, undef, undef, + -desc => "Example of a feature declaration by an extension", + -doc => "%PERLDOC{\"Foswiki::Extension::Empty\"}%", + ], + ; =begin TML @@ -111,7 +126,7 @@ The following subs implement this functionality: | =extBefore @nameList= | Extension must be placed before extensions in =@nameList= | | =extAfter @nameList= | Extension must be placed after extensions in =@nameList= | -What these do is define a directed graph of extensions. When all extensions are +These define a directed graph of extensions. When all extensions are loaded and registered the graph gets sorted using topoligical sort. The resulting order is stored in =Foswiki::ExtManager= =orderedList= attribute. @@ -205,13 +220,10 @@ object which actually initiated this callback; and reference to a hash with parameters supplied by the object – see =params= key of callback arguments in =Foswiki::Aux::Callbacks=. -__NOTE:__ The method arguments are different from common callback handler as -described in =Foswiki::Aux::Callbacks= because there is no point of passing the -=data= key of arguments hash. Instead extension callback method can rely on -object's internals whenever needed. - -See =Foswiki::Aux::Callbacks= - +%X% *NOTE:* The method arguments are different from common callback handler as +described in =%PERLDOC{Foswiki::Aux::Callbacks}%= because there is no point of +passing the =data= key of arguments hash. Instead extension callback method can +rely on object's internals whenever needed. =cut callbackHandler postConfig => sub { @@ -263,67 +275,140 @@ pluggable someMethod => sub { 1; ----++++ Notes on implementation details +---++++ Implementation Details Method overriding can only work within properly initialized %WIKITOOLNAME% application environment. I.e. it requires initialized extensions on application -object. On the other hand not only classes with =Foswiki::AppObject= role -applied can use this feature. This is because =Foswiki::Aux::_ExtensibleRole= -implicitly adds =__appObj= attribute to the class it is applied to. In -distinction of =Foswiki::AppObject= =app= attribute =__appObj= is not required -and can remain undefined. +object. On the other hand not only classes with =%PERLDOC{Foswiki::AppObject}%= +role applied can use this feature. This is because +=Foswiki::Aux::_ExtensibleRole= implicitly adds =__appObj= attribute to the +class it is applied to. In distinction to =%PERLDOC{"Foswiki::AppObject" +attr="app"}%= attribute =__appObj= is not required and can remain undefined. + +For a non =Foswiki::AppObject= class to get the feature enabled it is mandatory +to have objects of the class created with =%PERLDOC{"Foswiki::App" +method="create"}%= or =%PERLDOC{"Foswiki::AppObject" method="create"}%= methods. +For example (consider the above sample and assume that the code below belongs +to a class with =Foswiki::AppObject= role): + + +plugBefore "Foswiki::CoreClass::someMethod" => sub { + my $this = shift; + my ($params) = @_; + my $num = $params->{args}->[0]; + + say "This is obj", $num; +}; + +... + +sub testPlugBefore { + my $this = shift; + + my $obj1 = $this->app->create("Foswiki::CoreClass"); + my $obj2 = $this->create("Foswiki::CoreClass"); + my $obj3 = Foswiki::CoreClass->new; + + $obj1->someMethod(1); + $obj2->someMethod(2); + $obj3->someMethod(3); +} + -For a non-=Foswiki::AppObject= class to allow the feature during runtime it -is mandatory to create objects of this class using =Foswiki::App= or -=Foswiki::AppObject= =create()= methods. +This would output: + + +This is obj1 +This is obj2 + + +because =$obj3= knowns nothing about the application and correspondingly about +the extensions. ---++++ plugBefore, plugAround, plugAfter -In terms of =Moo= these subroutines are modifiers. But contrary to =Moo='s -implementation where, say, a _before_ modifier might not be called under cetain -conditions, all registered =plug*= modifiers are guaranteed to be executed -unless the execution flow gets interrupted by a modifier code. Those nuances -will be explained later in this documentation. +In terms of =CPAN:Moo= these subroutines are modifiers. All registered =plug*= +modifiers are guaranteed to be executed unless the +[[ChainedExecutionFlow][execution flow]] gets interrupted. When a pluggable method is called the extensions framework first executes all -_before_ methods; then _around_ ones; then _after_. Within each group methods -are called using the order defined by =Foswiki::ExtManager= =orderedList= -attribute (see the [[#ExtDeps][dependecies section]]). +_before_ methods; then _around_ ones and then possibly the original method; then +_after_. Within each group methods are called using the order defined by +=%PERLDOC{"Foswiki::ExtManager" attr="orderedList"}%= attribute (see the +[[#ExtDeps][dependecies section]]). -__NOTE:__ It is commonplace for _after_ methods to be called in reverse order. +%X% *NOTE:* It is commonplace for _after_ methods to be called in reverse order. But =plugAfter= order is straight, same as for =plugBefore= and =plugAround=. This is a subject for discussion and is very likely to change in the future. -Contrary to =Moo='s modifiers, methods registered with =plug*= modifiers are all -executed as _extenion_ methods, not as methods of an object of a core class. The -object is passed as a key =object= of parameters hashref in the second argument. -All keys of the hashref are in the followin table: +Contrary to =Moo= modifiers, methods registered with =plug*= modifiers are all +executed as _extenion object_ methods, not as methods of the object to which the +pluggable method belongs. I.e. their first argument =$this= points to the +extension object. The pluggable method's object is passed as key =object= of +parameters hashref in the second argument. + +Keys of the parameters hash are: | *Key* | *Type* | *Description* | -| =object= | blessed ref | The object the method is being called upon. | -| =class= | string | The class which has registered the pluggable method. Might be different from the above object's class if object was created using a subclass. | -| =method= | string | Name of the pluggable method registered by the class above. Could be useful for cases when same extension method is used to handle few different pluggable methods. | +| =object= | blessed ref | The object the pluggable method is being called upon. | +| =class= | string | The class which has registered the pluggable method. \ + Might be different from the object's class if it was created with a \ + subclass. | +| =method= | string | Name of the pluggable method registered by the class \ + above. Could be useful for cases when same extension method is used to \ + handle few different pluggable methods. | | =stage= | string | _before_, _around_, or _after_. | -| =args= | array ref | Reference to arguments array =@_= passed to the pluggable method. The array content can be changed by extension methods but the ref itself has to be left untouched. If a method changes it the extensions framework will restore the original value discarding all changes done by the method. Because this key points to =@_= then modification of =$n='th element has the same effect as modification of =$_[$n]=. | -| =wantarray= | scalar | =wantarray= function value for the pluggable method. | -| =rc= | anything | Pluggable method's return value. The original pluggable method won't be executed if any of _around_ methods sets this key to whatever (including =undef=) value. It's not allowed to be set by a _before_ method; if set then the framework will clean it up. | +| =args= | array ref | Reference to arguments array =@_= passed to the \ + pluggable method. The array content can be changed by extension methods but \ + the ref itself has to be left intact. If a method changes it the \ + extensions framework will restore the original reference discarding any \ + changes done by the modifier. Because the key points to =@_= then \ + modification of its =$n'th= element has the same effect as modification of \ + =$_[$n]=. | +| =wantarray= | scalar | =wantarray= for the pluggable method. | +| =rc= | anything | Pluggable method's return value. The original pluggable \ + method won't be executed if any of the _around_ modifiers sets this key to \ + whatever (including =undef=) value. It's not allowed to be set by a \ + _before_ method; if set then the framework would clean it up. | Methods can use the parameters hashref to communicate to each other by storing necessary information in it using unique key names. Generally it is recommended for an extension to take measures as to avoid clashing with other extensions. Though not being the most handy but the most reliable method would be to use -extension's name as the key where all extension-specific data is stored. +extension's name as the key where all extension-specific data is stored: + + +plugAround 'Foswiki::CoreClass::someMethod' => sub { + my $this = shift; + my ($params) = @_; + + $params->{'Ext::MyExtension'}{sayIt} = "Hi from around!"; +}; + +plugAfter 'Foswiki::CoreClass::someMethod' => sub { + my $this = shift; + my ($params) = @_; + + say "Around sent me this: ", $params->{'Ext::CoreClass'}{sayIt} + if defined $params->{'Ext::CoreClass'}{sayIt}; +}; + + +Note that it is ok to use same naming convention as for +%PERLDOC{"Foswiki::FeatureSet" section="Namespaces"}% namespaces. ---++++ Execution flow control +Read about general info about [[ChainedExecutionFlow][chained calls]] first. + A method can have influence over the execution flow by raising =Foswiki::Exception::Ext::Last= or =Foswiki::Exception::Ext::Restart= exceptions. -=Last= will stop the current group execution and pass the control over to the -ext group. I.e. if raised for _before_ chain then _around_ will be started; for -_around_ it'll be _after_. And for _after_ it will return to the calling code. -If the exception was supplied with =rc= parameter: +=Last= stops the current group execution and pass the control over to the +next group. I.e. if raised for _before_ chain then _around_ will be started; for +_around_ it'd be _after_. And for _after_ it will return to the calling code. +If the exception raised with =rc= parameter: Foswiki::Exception::Ext::Last->throw( rc => 0, ); @@ -378,40 +463,50 @@ subroutine. This is perhaps the most powerful feature of the extensions framework. Consider the following line of code: -extClass 'Foswiki::Config' => 'Foswiki::Extension::Empty::DBConfig'; +extClass 'Foswiki::Config' => 'Foswiki::Extension::Empty::Config'; -Every time the =create()= method is request to create an object of some class it -first consults the extensions framework if there is a subclass registered for -it. And if there is one it is used instead of the original. +Every time =%PERLDOC{"Foswiki::App" method="create"}%= method is requested to +create an object of some class it first consults the extensions framework if +there is a subclass registered for it. And if there is one it is used instead of +the original. Subclasses are created by the framework using the registrations from extensions. -Because it is possible for more than one extension to register a subclass for -the same core class the order of inheritance cannot be determined at the moment -of registration. For this reason all extensions are first loaded into memory and -then the framework analyses them and builds subclasses for every extension -registered core class. - -Due to the way =Moo= works a registered subclass module in fact must be a -=Moo::Role=. What the framework actually does then it creates a new class with -all registered subclasses being applied as roles in the order reverse to -=orderedList= attribute defined (think of the way inherited methods are called). -See -[[CPAN:Role::Tiny#create_class_with_roles][Role::Tiny::create_class_with_roles -method]]. The core class is used as the base. +Read more on how it works in +%PERLDOC{"Foswiki::ExtManager" section="Subclassing"}%. The only thing to +mention is that a registered =extClass= must be a [[CPAN:Moo::Role][role]]: + + +package Foswiki::Extension::Empty::Config; + +use Moo::Role; + +around readConfig => sub { + my $this = shift; + + say STDERR "Hey, this method must not be used anymore!"; + + Foswiki::Exception::Ext::Last->throw( + rc => 0, # Indicate that LSC cannot be loaded. + ); +}; + +1; + + +The sample class would intercept calls to deprecated +%PERLDOC{"Foswiki::Config" method="readConfig"}% method and make them fail by +returning _false_ value to the calling code making it think that config read +has failed. What could be done using this feature is limited by once imagination only. -=Foswiki::Extension::Empty::DBConfig= is used as an example subclass to give an -idea of storing the =LocalSite.cfg= in a database of some kind. While rewriting -the core class might be much of a burden somebody can simple create an extension -and implement this functionality. All an administrator of a wiki would have to -do then is to install the extension. And - voilà! – his configuration can now be -shared across multpile installations or even help to clusterize the setup. If -same extensions creator would then decide to implement a =Foswiki::Store= with -database support then it's one more step close to scalable %WIKITOOLNAME%. - -See =UnitTestContrib/test/unit/TestExtensions/Foswiki/Extension/Sample/Config.pm= -for an example of subclassing. Or test_subClassing= in =ExtensionsTests= test +Imagine a =Foswiki::Store= implementation with database support - it would be +one more step closer to scalable %WIKITOOLNAME% run on multiple servers. And +this could be done without rewriting the core, just by installing an extension! + +Check out +=UnitTestContrib/test/unit/TestExtensions/Foswiki/Extension/Sample/Config.pm= +for an example of subclassing. Or =test_subClassing= in =ExtensionsTests= test suite. =cut @@ -421,13 +516,12 @@ suite. =begin TML ----++ SEE ALSO +---++ Related -=Foswiki::ExtManager=, =Foswiki::Extension=, =Foswiki::Class=, and -=ExtensionsTests= test suite. +=%PERLDOC{Foswiki::ExtManager}%=, =%PERLDOC{Foswiki::Extension}%=, +=%PERLDOC{Foswiki::Class}%=, =ExtensionsTests= test suite. -Check out [[Foswiki:Development.OONewPluginModel][Foswiki topic]] where all this -once originated from. +Check out [[Foswiki:Development.OONewPluginModel][Foswiki proposal topic]] too. =cut diff --git a/core/data/System/FoswikiV3Essentials.txt b/core/data/System/FoswikiV3Essentials.txt index c91cc0f972..99abcf0f47 100644 --- a/core/data/System/FoswikiV3Essentials.txt +++ b/core/data/System/FoswikiV3Essentials.txt @@ -1,4 +1,4 @@ -%META:TOPICINFO{author="ProjectContributor" date="1507256707" format="1.1" version="1"}% +%META:TOPICINFO{author="ProjectContributor" date="1507428457" format="1.1" version="1"}% %META:TOPICPARENT{name="DeveloperDocumentationCategory"}% ---+ Foswiki v3 Essentials %TOC% @@ -17,9 +17,9 @@ Basically, what makes v3 different from all previous versions are: $ PSGI support : Yes, the new code is PSGI compliant. Basically, it is the main and only mode of operation now. Any other environments like CGI, FastCGI, etc. are now supported through corresponding CPAN:Plack::Handler adapters. + $ OO-redesigned test framework : It's all said. $ PSGI testing framework : It comes as an extension to the legacy testing code and wraps around CPAN:Plack::Test. - $ OO-redesigned test framework : It's all said. $ Manageable callbacks : Flexible and powerful, the new callback protocol unifies the interface and provides more control. $ Extensions : They're here to replace some day the old plugins framework. diff --git a/core/lib/Foswiki/Class.pm b/core/lib/Foswiki/Class.pm index d13ab6143a..86ff7ca586 100644 --- a/core/lib/Foswiki/Class.pm +++ b/core/lib/Foswiki/Class.pm @@ -10,8 +10,8 @@ use warnings; ---+ Package Foswiki::Class -This is a wrapper package for Moo and intended to be used as a replacement and -a shortcut for a bunch of code like: +This wrapper package for Moo intended to be used as a replacement and a shortcut +for a bunch of code like: use v5.14; diff --git a/core/lib/Foswiki/ExtManager.pm b/core/lib/Foswiki/ExtManager.pm index 961a4bf039..6597e90f14 100644 --- a/core/lib/Foswiki/ExtManager.pm +++ b/core/lib/Foswiki/ExtManager.pm @@ -33,18 +33,19 @@ and initialization. At the construction stage (which is called so because it's initiated by class constructor) the manager only loads extensions by scanning directories listed in -=extSubDirs= attribute and loading all _.pm_ files it finds there. The order of -files (and thus the order of extensions) in not relevant here and depends on how -=IO::Dir= =read()= method works. +=%PERLDOC{"Foswiki::ExtManager" attr="extSubDirs" text="extSubDirs"}%= attribute +and loading all _.pm_ files it finds there. The order of files (and thus the +order of extensions) in not relevant here and depends on how =CPAN:IO::Dir= +=read()= method works. Upon loading an extension module registers (or declares – particular term depends on a point of view) it's components or attributes like methods or callback handlers, subclasses, order in the execution chain (before or after another extension), etc. All this could either be done by direct reference to extension manager's =register*()= family of static methods; or, preferably, by -using subroutines exported by =Foswiki::Class= module when =extension= modifier -is used as a =Foswiki::Class= parameter (see =Foswiki::Extension::Empty= for -code examples). +using subroutines exported by =%PERLDOC{Foswiki::Class}%= module when +=extension= modifier is used as a =Foswiki::Class= parameter (see +=%PERLDOC{Foswiki::Extension::Empty}%= for code examples). *Important!* It has to be understood that after this stage is completed extensions are only registered with the core code. The information obtained is @@ -134,11 +135,18 @@ compatible: 1 It is a subclass of =Foswiki::Extension= 1 Its module defines either =$API_VERSION= or =@FS_REQUIRED= 1 Features required by the module (as defined in =@FS_REQUIRED=) are provided - either by the core or by specified namespaces (see =Foswiki::FeatureSet=) + either by the core or by specified namespaces (see + =%PERLDOC{Foswiki::FeatureSet}%=) 1 If no =@FS_REQUIRED= is found then the requested API version in =$API_VERSION= is tested to fall into inclusive range from =$Foswiki::ExtManager::MIN_VERSION= to =$Foswiki::ExtManager::VERSION=. +%X% *NOTE:* The =$Foswiki::ExtManager::VERSION= mentioned above is not Foswiki +version. It declares current API version which may fall behind +$Foswiki::VERSION if API did not change over few Foswiki releases. For example, +Foswiki v3.1 might easily have API versioned as v3.0.5. It is recommended to +set API version to match the release where it was changed. + The =@FS_REQUIRED= is a simple list of strings where each string is either a feature name or two strings together define a namespace in a form of =-namespace= option: @@ -190,16 +198,18 @@ An extension could subclass practically any core class and redefine its functionality. To make this possible any %WIKITOOLNAME% code, including extensions themselves, must comply to the following rules: - * Any class must be a direct or indirect descendant of =Foswiki::Object=. + * Any class must be a direct or indirect descendant of + =%PERLDOC{Foswiki::Object}%=. * New class instances (objects) must be created using - =Foswiki::App::create()= method which is directly available as a - ObjectMethod for classes consuming =Foswiki::AppObject= role. + =%PERLDOC{"Foswiki::App" method="create"}%= method which is directly + available as a ObjectMethod for classes consuming =Foswiki::AppObject= + role. The second rule is redundant for extensions because they're inheriting from =Foswiki::Extension= which already consumes the role. -Most of the core classes, with =Foswiki::App= in the first place, are following -these rules making it possible to subclass practically any core class. +Most of the core classes, with =%PERLDOC{Foswiki::App}%= in the first place, are +following these rules making it possible to subclass practically any core class. It is important remember the advise about multiple active extensions. With respect to subclassing the advise could be extended with an additional sentense: @@ -231,11 +241,12 @@ method. ---+++ Pluggable Methods Alongside with subclassing there is a less radical method of redefining core -behavior. It is called 'pluggable methods' and it depends on the good will of -a core class which can declare itself an _extensible_ (see =Foswiki::Class=). -With this modifier it acquires the power of declaring some of its methods -as _pluggables_. A pluggable method is extensions' plaything. But more details -on this subject can be found in =Foswiki::Extension::Empty= documentation. +behavior. It is called 'pluggable methods' and it depends on the good will of a +core class which can declare itself an _extensible_ (see +=%PERLDOC{Foswiki::Class}%=). With this modifier it acquires the power of +declaring some of its methods as _pluggables_. A pluggable method is extensions' +plaything. But more details on this subject can be found in +=Foswiki::Extension::Empty= documentation. =cut @@ -310,10 +321,10 @@ has extensions => ( List of *library* paths where to look for extensions. Those are not ultimate as full name of directories where extensions are actually being located is formed -using =extPrefix= attribute. For example, with it's default _Foswiki::Extension_ -value a library path _/usr/local/www/foswiki/lib_ would be used to make the full -form _/usr/local/www/foswiki/lib/Foswiki/Extension_ – and this is where the -manager is expecting to find extensions. +using =extPrefix= attribute. For example, with it's default +_"Foswiki::Extension"_ value a library path _/usr/local/www/foswiki/lib_ would +be used to make the full form _/usr/local/www/foswiki/lib/Foswiki/Extension_ – +and this is where the manager is expecting to find extensions. Lazy, builder uses the following sources to set the attribute (next one is used if none of the previous is set): @@ -1641,7 +1652,9 @@ sub registerPlugMethod { ---++ RELATED -=%PERLDOC{Foswiki::Extension::Empty}%=, =%PERLDOC{Foswiki::Class}%=. +%PERLDOC{Foswiki::Extension::Empty}% : a source of information on practical details of extension development + +%PERLDOC{Foswiki::Class}% =cut @@ -1649,7 +1662,7 @@ sub registerPlugMethod { __END__ Foswiki - The Free and Open Source Wiki, http://foswiki.org/ -Copyright (C) 2016 Foswiki Contributors. Foswiki Contributors +Copyright (C) 2016-2017 Foswiki Contributors. Foswiki Contributors are listed in the AUTHORS file in the root of this distribution. NOTE: Please extend that file, not this notice.