Difference between revisions of "Filters"

Jump to: navigation, search
m (Local configuration)
 
(32 intermediate revisions by 16 users not shown)
Line 1: Line 1:
<p class="note">'''Please note:''' This page contains information for developers. You may prefer to read the [[Filters 2.0| information about filters for teachers and administrators]]. Once Moodle 2.0 is released, [[Filters]] will be renamed to 'Development:Filters 1.9 and before' and this page will be renamed to 'Development:Filters'.</p>
+
<p class="note">'''Please note:''' This page contains information for developers. You may prefer to read the [[:en:Filters| information about filters for teachers and administrators]].</p>
 +
 
 +
{{Moodle 2.0}}
  
  
Line 8: Line 10:
 
The possibilities are endless. There are a number of standard filters included with Moodle, or you can create your own. Filters are one of the easiest types of plugin to create. This page explains how.
 
The possibilities are endless. There are a number of standard filters included with Moodle, or you can create your own. Filters are one of the easiest types of plugin to create. This page explains how.
  
==Two types of filter==
+
==Before you start==
  
Moodle supports two types of filter:
+
Go to  Site administration ▶ Plugins ▶ Filters ▶ Common filter settings and set Text cache lifetime to 0 ("No") while you do development. Otherwise, you will not be able to see the effects of your changes when you edit your filter's code. (You should also be using the other common developer settings, like developer debug, theme designer mode and so on.)
* Stand-alone filters. These live in a folder inside the 'filter' folder. For example, in 'filter/myfilter'. 'filter/tex' is an example of a core filter of this type.
 
* Filters that are part of an activity module. In this case, the filter code lives inside the 'mod/mymod' folder. 'mod/glossary' is an example of a core module with a filter.
 
The two types of filter work in exactly the same way, and to create one, you put your code in files with the same name, just in different places in the code base.
 
  
 
==Creating a basic filter==
 
==Creating a basic filter==
Line 23: Line 22:
 
2. Inside that folder, we create a file called 'filter.php'.
 
2. Inside that folder, we create a file called 'filter.php'.
  
3. Inside that PHP file, we define a class called helloworld_filter, that extends the moodle_text_filter class.
+
3. Inside that PHP file, we define a class called filter_helloworld, that extends the moodle_text_filter class. (Note that the file doesn't end by closing the php section with the '?>' tag. This is standard for Moodle and is used to avoid problems with trailing whitespace.)
 
<code php>
 
<code php>
 
<?php
 
<?php
class helloworld_filter extends moodle_text_filter {
+
class filter_helloworld extends moodle_text_filter {
 
     // ...
 
     // ...
 
}
 
}
?>
+
 
 
</code>
 
</code>
  
 
4. Inside that class, we have to define one method, called 'filter'. This takes the HTML to be filtered as an argument. The method should then transform that, and return the processed text. Replace the '// ...' above with
 
4. Inside that class, we have to define one method, called 'filter'. This takes the HTML to be filtered as an argument. The method should then transform that, and return the processed text. Replace the '// ...' above with
 
<code php>
 
<code php>
class helloworld_filter extends moodle_text_filter {
+
class filter_helloworld extends moodle_text_filter {
 
     public function filter($text, array $options = array()) {
 
     public function filter($text, array $options = array()) {
 
         return str_replace('world', 'hello world!', $text);
 
         return str_replace('world', 'hello world!', $text);
Line 40: Line 39:
 
}
 
}
 
</code>
 
</code>
 +
 +
5. version.php
 +
The version.php file keeps track of the version of your module, and other attributes, and is required for newer moodle versions. For a full list of the attributes please see [[version.php]].
 +
Place the version.php in your 'filter/helloworld' directory.
 +
<code php>
 +
defined('MOODLE_INTERNAL') || die();
 +
$plugin->version  = 2016052300;        // The current plugin version (Date: YYYYMMDDXX)
 +
$plugin->requires  = 2016051900;        // Requires this Moodle version
 +
$plugin->component = 'filter_helloworld'; // Full name of the plugin (used for diagnostics)
 +
</code>
 +
  
 
That is basically all there is to it!
 
That is basically all there is to it!
Line 49: Line 59:
 
When you do, you will find that your plugin does not have a name. We missed a step:
 
When you do, you will find that your plugin does not have a name. We missed a step:
  
5. Inside the 'filter/helloworld' folder, create a folder called 'lang', and in there, create a folder called 'en'.
+
6. Inside the 'filter/helloworld' folder, create a folder called 'lang', and in there, create a folder called 'en'.
  
6. Inside there, create a file called 'filter_helloworld.php'. That is, you have just created the file 'filter/helloworld/lang/en/filter_helloworld.php'.
+
7. Inside there, create a file called 'filter_helloworld.php'. That is, you have just created the file 'filter/helloworld/lang/en/filter_helloworld.php'.
  
7. In that file, put
+
8. In that file, put
 
<code php>
 
<code php>
<?php // $Id$
+
<?php
// Language string for filter/helloworld.
 
  
 
$string['filtername'] = 'Hello world!';
 
$string['filtername'] = 'Hello world!';
 +
$string['pluginname'] = 'Hello world!';
 
</code>
 
</code>
  
That may seem a little involved, just to give your filter a name, but it is just [[Places_to_search_for_lang_strings|the standard way Moodle stores language strings for plugins]].
+
See [[String API]] for details.
  
 
==Trying out your filter==
 
==Trying out your filter==
Line 67: Line 77:
 
We had just got to the [[Filters|filters administration screen]]. If you reload that page now, it should now show your filter with its proper name. Turn your filter on now.
 
We had just got to the [[Filters|filters administration screen]]. If you reload that page now, it should now show your filter with its proper name. Turn your filter on now.
  
Filters are applied to all text that is printed with the [[Output functions|output functions]] format_text(), and, if you have turned on that option, format_string(). So, to see your filter in action, add some content containing the work 'world' somewhere, for example, create a test course, and use the work in the course description. When you look at that course in the course listing, you should see that your filter has transformed it.
+
Filters are applied to all text that is printed with the [[Output functions|output functions]] format_text(), and, if you have turned on that option, format_string(). So, to see your filter in action, add some content containing the word 'world' somewhere, for example, create a test course, and use the word in the course description. When you look at that course in the course listing, you should see that your filter has transformed it.
  
 
==Adding a global settings screen==
 
==Adding a global settings screen==
Line 73: Line 83:
 
Some filters can benefit from some settings to let the administrator control how they work. Suppose we want to greet something other than 'world'. To add global settings to the filter you need to:
 
Some filters can benefit from some settings to let the administrator control how they work. Suppose we want to greet something other than 'world'. To add global settings to the filter you need to:
  
8. Create a file called 'filtersettings.php' inside the 'filter/helloworld' folder.
+
9. Create a file called 'filtersettings.php' inside the 'filter/helloworld' folder. Use standard 'settings.php' file in Moodle 2.6 and later.
  
9. In the 'filtersettings.php' file, put something like:
+
10. In the 'filtersettings.php' file, put something like:
 
<code php>
 
<code php>
$settings->add(new admin_setting_configtext('filter_helloworld_word',
+
$settings->add(new admin_setting_configtext('filter_helloworld/word',
 
         get_string('word', 'filter_helloworld'),
 
         get_string('word', 'filter_helloworld'),
         get_string('configword', 'filter_helloworld'), 'world', PARAM_NOTAG));
+
         get_string('word_desc', 'filter_helloworld'), 'world', PARAM_NOTAGS));
 
</code>
 
</code>
  
10. In the language file 'filter/helloworld/lang/en/filter_helloworld.php' add the necessary strings:
+
11. In the language file 'filter/helloworld/lang/en/filter_helloworld.php' add the necessary strings:
 
<code php>
 
<code php>
 
$string['word'] = 'The thing to greet';
 
$string['word'] = 'The thing to greet';
$string['configword'] = 'The hello world filter will add the word \'hello\' in front of every occurrence of this word in any content.';
+
$string['word_desc'] = 'The hello world filter will add the word \'hello\' in front of every occurrence of this word in any content.';
 
</code>
 
</code>
  
11. Change the filter to use the new setting:
+
12. Change the filter to use the new setting:
 
<code php>
 
<code php>
class helloworld_filter extends moodle_text_filter {
+
class filter_helloworld extends moodle_text_filter {
 
     public function filter($text, array $options = array()) {
 
     public function filter($text, array $options = array()) {
         global $CFG;
+
         $word = get_config('filter_helloworld', 'word');
         return str_replace($CFG->filter_helloworld_word,
+
         return str_replace($word, "hello $word!", $text);
                "hello $CFG->filter_helloworld_word!", $text);
 
 
     }
 
     }
 
}
 
}
Line 104: Line 113:
  
 
One important thing to remember when creating a filter is that the filter will be called to transform every bit of text output using format_text(), and possibly also format_string(). That means that you have to be careful, or you could cause big performance problems. If you have to get data out of the database, try to cache it so that you only do a fixed number of database queries per page load. The Glossary filter is an example of this. (I am not sure how good an example ;-))
 
One important thing to remember when creating a filter is that the filter will be called to transform every bit of text output using format_text(), and possibly also format_string(). That means that you have to be careful, or you could cause big performance problems. If you have to get data out of the database, try to cache it so that you only do a fixed number of database queries per page load. The Glossary filter is an example of this. (I am not sure how good an example ;-))
 +
 +
If your filter uses a special syntax or it is based on an appearance of
 +
a substring in the text, it is recommend to perform a quick and cheap
 +
<tt>strpos()</tt> search first prior to executing the full regex-based search and replace.
 +
 +
<code php>
 +
/**
 +
* Example of a filter that uses <a> links in some way.
 +
*/
 +
public function filter($text, array $options = array()) {
 +
 +
    if (!is_string($text) or empty($text)) {
 +
        // Non-string data can not be filtered anyway.
 +
        return $text;
 +
    }
 +
 +
    if (stripos($text, '</a>') === false) {
 +
        // Performance shortcut - if there is no </a> tag, nothing can match.
 +
        return $text;
 +
    }
 +
 +
    // Here we can perform some more complex operations with the <a>
 +
    // links in the text.
 +
}
 +
</code>
  
 
==Local configuration==
 
==Local configuration==
  
In addition, in Moodle 2.0, filters can also have different configuration in each context. For example, the glossary could be changes so that in Forum A, you can choose to only link words from a particular glossary, sat Glossary A, while in Forum B you choose to link words from Glossary B.
+
In addition, in Moodle 2.0, filters can also have different configuration in each context. For example, the glossary filter could be changed so that in Forum A, you can choose to only link words from a particular glossary, say Glossary A, while in Forum B you choose to link words from Glossary B.
  
 
To do that sort of thing, you need to add a file called filterlocalsettings.php. In it, you must define a [[lib/formslib.php|Moodle form]] that is a subclass of filter_local_settings_form. In addition to the standard formslib methods, you also need to define a save_changes method. There is not a good example of this in the standard Moodle install yet. To continue our example:
 
To do that sort of thing, you need to add a file called filterlocalsettings.php. In it, you must define a [[lib/formslib.php|Moodle form]] that is a subclass of filter_local_settings_form. In addition to the standard formslib methods, you also need to define a save_changes method. There is not a good example of this in the standard Moodle install yet. To continue our example:
  
12. Create a file called 'filterlocalsettings.php' inside the 'filter/helloworld' folder.
+
13. Create a file called 'filterlocalsettings.php' inside the 'filter/helloworld' folder.
  
13. In the 'filterlocalsettings.php' file, put:
+
14. In the 'filterlocalsettings.php' file, put:
 
<code php>
 
<code php>
 
class helloworld_filter_local_settings_form extends filter_local_settings_form {
 
class helloworld_filter_local_settings_form extends filter_local_settings_form {
Line 121: Line 155:
 
     }
 
     }
 
}
 
}
?>
+
 
 
</code>
 
</code>
  
14. Extend the filter to use the new setting, if it is present. The filter must be able to work if the setting is not set, for example by falling back to the global or default setting in this case
+
15. Extend the filter to use the new setting, if it is present. The filter must be able to work if the setting is not set, for example by falling back to the global or default setting in this case
 
<code php>
 
<code php>
 
<?php
 
<?php
class helloworld_filter extends moodle_text_filter {
+
class filter_helloworld extends moodle_text_filter {
 
     public function filter($text, array $options = array()) {
 
     public function filter($text, array $options = array()) {
 
         global $CFG;
 
         global $CFG;
Line 133: Line 167:
 
             $word = $this->localconfig['word'];
 
             $word = $this->localconfig['word'];
 
         } else {
 
         } else {
             $word = $CFG->filter_helloworld_word;
+
             $word = $CFG->filter_helloworld/word;
 
         }
 
         }
 
         return str_replace($word, "hello $word!", $text);
 
         return str_replace($word, "hello $word!", $text);
 
     }
 
     }
 
}
 
}
?>
+
 
 
</code>
 
</code>
 +
 +
==Two types of filter==
 +
 +
In the past, Moodle supported two different types of filter:
 +
* Stand-alone filters like the one we created above. These live in a folder inside the 'filter' folder. For example, in 'filter/myfilter'. 'filter/tex' is an example of a core filter of this type.
 +
* Filters that were part of an activity module. In this case, the filter code lives inside the 'mod/mymod' folder. 'mod/glossary' used to be an example of a core module with a filter.
 +
The second option no longer exists in Moodle 2.5 and later. All filters live in the filter folder. Of course, a filter may depend on an associated other plugin, like mod_glossary. If so, you should declare that in the [[version.php]] file.
 +
 +
==Dynamic content==
 +
 +
From Moodle 2.7:
 +
On (very few) pages - it is possible that page content is loaded by ajax *after* the page is loaded (e.g. equations in a glossary popup). In certain filter types (e.g. MathJax) javascript is required to be run on the output of the filter in order to do the final markup. For these types of filters, a javascript event is triggered when new content is added to the page (the content will have already been processed by the filter in php). The javascript for a filter can listen for these event notifications and reprocess the affected dom nodes.
 +
 +
To subscribe to the event:
 +
 +
        // Listen for events triggered when new text is added to a page that needs                                                 
 +
        // processing by a filter.                                                                                                 
 +
        Y.on(M.core.event.FILTER_CONTENT_UPDATED, this.contentUpdated, this);
 +
 +
 +
To handle the event:
 +
 +
    /**                                                                                                                           
 +
    * Handle content updated events - typeset the new content.                                                                   
 +
    * @method contentUpdated                                                                                                     
 +
    * @param Y.Event - Custom event with "nodes" indicating the root of the updated nodes.                                       
 +
    */                                                                                                                           
 +
    contentUpdated: function(event) {                                                                                             
 +
        var self = this;                                                                                                           
 +
        Y.use('mathjax', function() {                                                                                             
 +
            self._setLocale();                                                                                                     
 +
            event.nodes.each(function (node) {                                                                                     
 +
                node.all('.filter_mathjaxloader_equation').each(function(node) {                                                   
 +
                    MathJax.Hub.Queue(["Typeset", MathJax.Hub, node.getDOMNode()]);                                               
 +
                });                                                                                                               
 +
            });                                                                                                                   
 +
        });                                                                                                                       
 +
    }                     
 +
 +
See: filter/mathjaxloader/yui/src/loader/js/loader.js
  
 
==See also==
 
==See also==
  
* The example filter built here is in contrib CVS at http://cvs.moodle.org/contrib/plugins/filter/helloworld/
 
* [[Filters]] how to write filters for Moodle 1.9 and before. Note that a Moodle 1.9 filter will still work in Moodle 2.0, but you should still update your code when you get the chance.
 
 
* [[Filters schema]] - a page containing some ideas and thoughts about modifications to the filters system
 
* [[Filters schema]] - a page containing some ideas and thoughts about modifications to the filters system
* [[Filters 2.0]] - user documentation about filters.
+
* [[:en:Filters]] user documentation about filters.
 +
* [https://moodle.org/plugins/browse.php?list=category&id=7 - List of filters in the Plugins database].
 +
*[https://github.com/richardjonesnz/moodle_filter_simplemodal - A simple modal filter template using AMD rather than YUI].
  
[[Category:Filters]]
 
 
[[Category:Filter]]
 
[[Category:Filter]]
 +
[[Category:Plugins]]

Latest revision as of 08:50, 26 April 2019

Please note: This page contains information for developers. You may prefer to read the information about filters for teachers and administrators.

Moodle 2.0



Filters are a way to automatically transform content before it is output. For example

  • render embedded equations to images (the TeX filter)
  • Links to media files can be automatically converted to an embedded applet for playing the media.
  • Mentions of glossary terms can be automatically converted to links.

The possibilities are endless. There are a number of standard filters included with Moodle, or you can create your own. Filters are one of the easiest types of plugin to create. This page explains how.

Before you start

Go to Site administration ▶ Plugins ▶ Filters ▶ Common filter settings and set Text cache lifetime to 0 ("No") while you do development. Otherwise, you will not be able to see the effects of your changes when you edit your filter's code. (You should also be using the other common developer settings, like developer debug, theme designer mode and so on.)

Creating a basic filter

During this tutorial, we will build a simple example filter. We will make one that adds the word 'hello' before every occurrence of the word 'world'.

1. Since our filter is not part of a module, we should put it inside the 'filter' folder. Therefore, we create a directory called 'filter/helloworld'.

2. Inside that folder, we create a file called 'filter.php'.

3. Inside that PHP file, we define a class called filter_helloworld, that extends the moodle_text_filter class. (Note that the file doesn't end by closing the php section with the '?>' tag. This is standard for Moodle and is used to avoid problems with trailing whitespace.)

<?php
class filter_helloworld extends moodle_text_filter {
    // ...
}

4. Inside that class, we have to define one method, called 'filter'. This takes the HTML to be filtered as an argument. The method should then transform that, and return the processed text. Replace the '// ...' above with

class filter_helloworld extends moodle_text_filter {
    public function filter($text, array $options = array()) {
        return str_replace('world', 'hello world!', $text);
    }
}

5. version.php The version.php file keeps track of the version of your module, and other attributes, and is required for newer moodle versions. For a full list of the attributes please see version.php. Place the version.php in your 'filter/helloworld' directory.

defined('MOODLE_INTERNAL') || die();
$plugin->version   = 2016052300;        // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires  = 2016051900;        // Requires this Moodle version
$plugin->component = 'filter_helloworld'; // Full name of the plugin (used for diagnostics)


That is basically all there is to it!

Giving your filter a name

To try the new filter, you first have to log in as Administrator and enable it by going to the page Administration ► Plugins ► Filters ► Manage filters.

When you do, you will find that your plugin does not have a name. We missed a step:

6. Inside the 'filter/helloworld' folder, create a folder called 'lang', and in there, create a folder called 'en'.

7. Inside there, create a file called 'filter_helloworld.php'. That is, you have just created the file 'filter/helloworld/lang/en/filter_helloworld.php'.

8. In that file, put

<?php
 
$string['filtername'] = 'Hello world!';
$string['pluginname'] = 'Hello world!';

See String API for details.

Trying out your filter

We had just got to the filters administration screen. If you reload that page now, it should now show your filter with its proper name. Turn your filter on now.

Filters are applied to all text that is printed with the output functions format_text(), and, if you have turned on that option, format_string(). So, to see your filter in action, add some content containing the word 'world' somewhere, for example, create a test course, and use the word in the course description. When you look at that course in the course listing, you should see that your filter has transformed it.

Adding a global settings screen

Some filters can benefit from some settings to let the administrator control how they work. Suppose we want to greet something other than 'world'. To add global settings to the filter you need to:

9. Create a file called 'filtersettings.php' inside the 'filter/helloworld' folder. Use standard 'settings.php' file in Moodle 2.6 and later.

10. In the 'filtersettings.php' file, put something like:

$settings->add(new admin_setting_configtext('filter_helloworld/word',
        get_string('word', 'filter_helloworld'),
        get_string('word_desc', 'filter_helloworld'), 'world', PARAM_NOTAGS));

11. In the language file 'filter/helloworld/lang/en/filter_helloworld.php' add the necessary strings:

$string['word'] = 'The thing to greet';
$string['word_desc'] = 'The hello world filter will add the word \'hello\' in front of every occurrence of this word in any content.';

12. Change the filter to use the new setting:

class filter_helloworld extends moodle_text_filter {
    public function filter($text, array $options = array()) {
        $word = get_config('filter_helloworld', 'word');
        return str_replace($word, "hello $word!", $text);
    }
}

In standard Moodle, the censor, mediaplugin and tex filters all provide good examples of of how filters use global configuration like this.

A note about performance

One important thing to remember when creating a filter is that the filter will be called to transform every bit of text output using format_text(), and possibly also format_string(). That means that you have to be careful, or you could cause big performance problems. If you have to get data out of the database, try to cache it so that you only do a fixed number of database queries per page load. The Glossary filter is an example of this. (I am not sure how good an example ;-))

If your filter uses a special syntax or it is based on an appearance of a substring in the text, it is recommend to perform a quick and cheap strpos() search first prior to executing the full regex-based search and replace.

/**
 * Example of a filter that uses <a> links in some way.
 */
public function filter($text, array $options = array()) {
 
    if (!is_string($text) or empty($text)) {
        // Non-string data can not be filtered anyway.
        return $text;
    }
 
    if (stripos($text, '</a>') === false) {
        // Performance shortcut - if there is no </a> tag, nothing can match.
        return $text;
    }
 
    // Here we can perform some more complex operations with the <a>
    // links in the text.
}

Local configuration

In addition, in Moodle 2.0, filters can also have different configuration in each context. For example, the glossary filter could be changed so that in Forum A, you can choose to only link words from a particular glossary, say Glossary A, while in Forum B you choose to link words from Glossary B.

To do that sort of thing, you need to add a file called filterlocalsettings.php. In it, you must define a Moodle form that is a subclass of filter_local_settings_form. In addition to the standard formslib methods, you also need to define a save_changes method. There is not a good example of this in the standard Moodle install yet. To continue our example:

13. Create a file called 'filterlocalsettings.php' inside the 'filter/helloworld' folder.

14. In the 'filterlocalsettings.php' file, put:

class helloworld_filter_local_settings_form extends filter_local_settings_form {
    protected function definition_inner($mform) {
        $mform->addElement('text', 'word', get_string('word', 'filter_helloworld'), array('size' => 20));
        $mform->setType('word', PARAM_NOTAGS);
    }
}

15. Extend the filter to use the new setting, if it is present. The filter must be able to work if the setting is not set, for example by falling back to the global or default setting in this case

<?php
class filter_helloworld extends moodle_text_filter {
    public function filter($text, array $options = array()) {
        global $CFG;
        if (isset($this->localconfig['word'])) {
            $word = $this->localconfig['word'];
        } else {
            $word = $CFG->filter_helloworld/word;
        }
        return str_replace($word, "hello $word!", $text);
    }
}

Two types of filter

In the past, Moodle supported two different types of filter:

  • Stand-alone filters like the one we created above. These live in a folder inside the 'filter' folder. For example, in 'filter/myfilter'. 'filter/tex' is an example of a core filter of this type.
  • Filters that were part of an activity module. In this case, the filter code lives inside the 'mod/mymod' folder. 'mod/glossary' used to be an example of a core module with a filter.

The second option no longer exists in Moodle 2.5 and later. All filters live in the filter folder. Of course, a filter may depend on an associated other plugin, like mod_glossary. If so, you should declare that in the version.php file.

Dynamic content

From Moodle 2.7: On (very few) pages - it is possible that page content is loaded by ajax *after* the page is loaded (e.g. equations in a glossary popup). In certain filter types (e.g. MathJax) javascript is required to be run on the output of the filter in order to do the final markup. For these types of filters, a javascript event is triggered when new content is added to the page (the content will have already been processed by the filter in php). The javascript for a filter can listen for these event notifications and reprocess the affected dom nodes.

To subscribe to the event:

       // Listen for events triggered when new text is added to a page that needs                                                  
       // processing by a filter.                                                                                                  
       Y.on(M.core.event.FILTER_CONTENT_UPDATED, this.contentUpdated, this);


To handle the event:

   /**                                                                                                                             
    * Handle content updated events - typeset the new content.                                                                     
    * @method contentUpdated                                                                                                       
    * @param Y.Event - Custom event with "nodes" indicating the root of the updated nodes.                                         
    */                                                                                                                             
   contentUpdated: function(event) {                                                                                               
       var self = this;                                                                                                            
       Y.use('mathjax', function() {                                                                                               
           self._setLocale();                                                                                                      
           event.nodes.each(function (node) {                                                                                      
               node.all('.filter_mathjaxloader_equation').each(function(node) {                                                    
                   MathJax.Hub.Queue(["Typeset", MathJax.Hub, node.getDOMNode()]);                                                 
               });                                                                                                                 
           });                                                                                                                     
       });                                                                                                                         
   }                      

See: filter/mathjaxloader/yui/src/loader/js/loader.js

See also