Conditions: if this and that

n: smd_if | v: 1.0.0 | f: /

Documentation for the Textpattern plugin smd_if by Stef Dawson follows this short message from our sponsor ;-)

Plugin list button Plugin download button Compressed plugin download button


A generic ‘if condition’ tester. Can test any field or variable in the current article, file, image, link, URL var, <txp:variable /> or PHP context for a variety of attributes and take action if TRUE or FALSE.


  • Supports most major article, file, image and link variables such as section, category, custom fields, id, query string, author, body, excerpt, yahde yahde, plus url vars, server vars, txp vars, php vars, and sub-category/parent checking
  • Tests include equality, inequality, less than, greater than, divisible by, empty, used, defined, begins, ends, contains, is numeric / alpha / alphanumeric / lowercase / uppercase, among others
  • Tests for more than one condition at once and applies either AND logic (all tests must pass) or OR logic (any test must pass)
  • All tested fields and values are available to the container so you can display them
  • Custom regular-expression filters are available to help weed out bad data
  • Ugly and very dirty. Uses PHP’s eval() command which most programmers concur should be renamed evil()

Installation / Uninstallation

Download the plugin from either, or the software page, paste the code into the Txp Admin->Plugins pane, install and enable the plugin. Visit the forum thread for more info or to report on the success or otherwise of the plugin.

To uninstall, simply delete from the Admin->Plugins page.


Use the tag in any page, form or article context. Can also be used inside file, image, or link lists to take action depending on attributes of the current item.


At the place you wish to compare a field with another value, put this tag with at least the field attribute. Specify each field you wish to test as a series of comma-separated items — though the comma is overridable with param_delim. If the result of all the comparison tests is TRUE the contained block will be executed. If the result is FALSE, any <txp:else /> will be executed instead. Without any <txp:else /> block, nothing is output if the result is FALSE.


Comma-separated list of fields to test.
Comma-separated list of operations for comparison with the corresponding field (e.g. eq, not, begins, etc).
Default: eq for most tests, contains for parent tests
Comma-separated list of values with which to compare the corresponding fields.
How multiple tests are joined: Choose from:
and: all conditions must be met for a TRUE result.
or: any of the conditions that match will give a TRUE result.
Default: and
Whether to perform case-sensitive comparisons. Note that if using islower or isupper in a comparison, case sensitivity will automatically be switched on while the test is taking place. Values:
1 = yes
0 = no
Default: 0
List of regular expressions with which to filter one or more fields/values. See filtering.
List of items with which to replace each of the matching filters.
Limit the filter to certain types of test. Choose from any of:
or any field name
Default: all
Apply the filter to either the field, the value or both. Please see the filtering caveat below for important information about this attribute.
Default: field
Delimiter used between each field, operator or value in a multi-test. You normally only need to change this if you have used that character in the name of a custom field, for example.
Default: comma (,)
Delimiter used when specifying field or operator modifiers for:
urlvar, postvar, svrvar, txpvar
parent from LVL and CAT
separating an operator from the NUM or NOSPACE modifiers
Default: colon (:)
Delimiter used when specifying a list of values to check via the in, notin, between and range operators.
Default: forward-slash (/)
Prefix any replacement variable names with this string. If nesting smd_if tags, you will probably need to specify this on any inner smd_if tags to prevent the {replacement} variables clashing.
Default: smd_if_

The lists are processed in order, i.e. the 1st field uses the 1st operator in the list and compares it to the 1st value; the 2nd field uses the 2nd operator and compares it to the 2nd value, and so on. Values should usually be specified with placeholders to maintain order: e.g. value=", 4,, top"

Note that, although the first three attributes are usually mandatory, if you happen to require the default operator for all your tests, you can safely omit operator. Similarly with values: if you are entirely testing the existence or type of variables, you can omit the value parameter if you wish. And if you are testing the same field again and again for differing conditions, you can list it just once as a convenient shortcut.


List of field locations to look at. A non-exhaustive list of some useful values are:

  • s (global section) or section (article section)
  • c (global category) or category1, or category2 if on an article page
  • authorid or author
  • id (file/link/image/article ID). In an individual article context, use thisid instead
  • query (the query string from the search form)
  • pg (the current page number in lists)
  • month (current month field from the address bar)
  • status (document status: 200, 404, 403, etc)
  • page (the Txp Page template used by this section)
  • next_id / prev_id (ID of next/prev document if on article page)
  • next_title / prev_title(Title of next/prev document if on article page)
  • next_utitle / prev_utitle (url-title of next/prev document if on article page)
  • permlink_mode (take action based on one of the messy/clean URL schemes)
  • other article/file/link field (e.g. body, excerpt, article_image, keywords, linkanme, filename, downloads, …)
  • my_custom_field_name (any custom fields you have defined)
  • urlvar:var_name (any variable in the address bar after the ?)
  • postvar:var_name (any variable posted from an HTML form)
  • svrvar:var_name (any standard server variable, e.g. HTTP_USER_AGENT)
  • txpvar:my_var (any Textpattern variable set with <txp:variable name="my_var" />)
  • phpvar:my_var (any PHP variable in the global scope)
  • parent:optional_modifiers (whether the given category is a descendent of another category)
  • NULL (useful when comparing arbitrary values for emptiness)

If you specify a field name that does not exist, the text you use will be taken verbatim in most cases.

To avoid ambiguity you can prefix the field name with one of pretext, article, image, link or file, separating it from the field by mod_delim.

If you wish to compare a field that might contain HTML (e.g body), add the modifier :NOTAGS to the end of the field. It will have its HTML and PHP tags stripped from it and will also be trimmed to remove leading and trailing spaces. You may choose to solely remove spaces from both the start and end of any field by adding :TRIM to the end of the field.

If you suspect a field might contain HTMLish input like <, >, & or quotes/apostrophes you can elect to convert them to entities like &lt;, &quot;, etc. Just specify the :ESC modifier to replace everything except apostrophes with its entity equivalent, or use :ESCALL to include apostrophes.

The special field parent checks the parent category for a match. Unlike the other field types, the default operator for parent is ‘contains’. This is because the entire tree is checked for a match, starting from the top of the tree down to the current category. Internally, the plugin makes up a “breadcrumb trail” of categories in the current branch, each separated by a space, so testing for equality would require putting them all in the value parameter.

You are of course free to choose an alternative operator; begins is very useful for testing if the top level category matches the one given in the value field.

If you use parent:LVLn, the comparison will be restricted to that “level” of sub-category; LVL1 is the “top” level, LVL2 is the next sub-category level, and so on. When using these modifiers, the ‘eq’ operator becomes more useful because you are comparing a single parent category.

If you wish to compare against the category’s title instead of its name, add the :TTL modifier. To test the number of children the given category has, specify :KIDS. Note that TTL and KIDS are mutually exclusive and if they are both employed, the last one used takes priority.

When using articles, you can further modify the behaviour of the parent using the CATn syntax (where ‘n’ is 1 or 2). Specifying “parent” without CATn will use the global category (?c=). If you add :CATn it will instead compare the article’s category1 or category2 respectively.

You can use CAT, LVL and TTL/KIDS in combination, independently or not at all. This allows comparisons such as “if the 2nd sub-category of category1 equals blahblah” or “if category2 is a child of blahblah”. See Example 4.

One other special field is NULL. This is exactly what it says it is: empty. The reason for its inclusion is that sometimes you wish to test something that isn’t a true variable — e.g. a replacement variable from smd_vars or smd_each — to see if it’s empty or not.

If you were to put this:

<txp:smd_if field="{result}" operator="isempty">

you would not get the result you expect (it’s pretty esoteric but it revolves around the fact that "" (as a variable name) is not empty, it’s invalid). To get round this you may use NULL as a placeholder and move the thing you want to check into the value instead, e.g:

<txp:smd_if field="NULL" operator="eq" value="{result}">

will test the NULL object (i.e. ‘emptiness’) to see if it’s equal to the {result} replacement variable. You can use similar logic to test for optional variables by swapping the field and value, like this:

<txp:smd_if field="7" operator="gt" value="{result}">

That would see if the replacement variable {result} was less than or equal to 7 (that’s not a typo, the logic is reversed in this case: it is interpreted as: “is 7 greater than {result}”, which is the same as “is {result} less than or equal to 7”!)


List of operators to apply, in order, to each field. Choose from:

  • eq Equal (the default for all except ‘parent’)
  • not Not equal
  • lt Less than
  • gt Greater than
  • le Less than or equal to
  • ge Greater than or equal to
  • in Field is one of a list of values (use list_delim to separate values)
  • notin Field is not one of the given list of values (use list_delim to separate values)
  • divisible Field is exactly divisible by the value
  • between Field lies between the given values, exclusive (use list_delim to separate values)
  • range Field is within the range of given values, inclusive (use list_delim to separate values)
  • begins Field begins with a sequence of characters
  • contains Field contains a sequence of characters (default for ‘parent’)
  • ends Field ends with a sequence of characters
  • isempty Field is empty (contains nothing)
  • isused Field has some value
  • defined Field is set (useful with urlvar variables)
  • undefined Field is not set, or missing from the URL line
  • isnum Field is a number
  • isalpha Field contains characters only
  • isalnum Field contains alphanumeric characters only
  • islower Every character in the field is lower case
  • isupper Every character in the field is upper case
  • ispunct Every character in the field is some punctuation mark
  • isspace Every character in the field is a whitespace character (or tab, newline, etc)

With the comparison operators (primarily gt, lt, ge, le) you may find odd behaviour when comparing numbers. For example, urlvar:pic gt 6 will return TRUE if pic is set to “fred”. This is because the word “fred” (or at least the “character f”) is greater in the ASCII table than the “character 6”.

To circumvent this problem, you may append :NUM to the end of any of these operators to force the plugin to check that the values are integers.

If you wish to compare the length of a field, append :LEN to any of the numerical comparison operators. It doesn’t make sense to use both :NUM and :LEN together, so they are mutually exclusive.

If you wish to compare the quantity (count) of a list of items in a field, append :COUNT to any of the numerical comparison operators. Again, this option is mutually exclusive with all other modifiers.

There is a subtle difference between the operators between and range; the former does not include the endpoint values you specify whereas the latter does. For example, the value 10 is in the range 10/20 but it is not between 10/20. The value 11, however, satisfies both. You can use the :NUM modifier for these two operators, though most tests will work fine without it, and you may check if list :COUNT values lie within two endpoints. You can also compare non-integer values with these operators.

The begins, ends and contains operators, along with any of the is operators (except isspace), can take an extra parameter as well. Since they compare every character against the given behaviour, space characters can mess things up a bit. For example field="custom1" operator="islower" will fail if custom1 contains “this is a test”. Or comparing something to body can fail because the body often starts with a number of space characters. To circumvent this, add :NOSPACE to the operator which will remove all spaces from the string before testing it.

Note also that while defined and undefined differ semantically from isused and isempty (respectively), the way Txp assigns variables means that, for the most part, the terms are interchangeable. When dealing with urlvars, postvars and svrvars, the two sets of operators behave independently, as you would expect. See Example 5 for more. Neither defined nor undefined make sense with parent, so they are forbidden.


List of values to compare each field in turn to. Can be static values/text or the name of any Txp field, like those given in field (except “parent”).

To distinguish a Txp field from static text, prefix the field name with ?. For example: value="title" will compare your chosen field against the word “title”, whereas value="?title" will compare your field against the current article’s title.

If you wish to compare a value that might contain HTML (e.g ?body), add the modifier :NOTAGS to the end of the value. It will have any HTML and PHP tags stripped from it and will also be trimmed to remove leading and trailing spaces. You may choose to solely remove spaces from both the start and end of any value by adding :TRIM to the end of the value.

If you suspect a value might contain HTMLish input like <, >, & or quotes/apostrophes you can elect to convert them to entities like &gt;, &quot;, etc. Just specify the :ESC modifier to replace everything except apostrophes with its entity equivalent, or use :ESCALL to include apostrophes.

Note that you may find using double-quotes in fields gives unexpected results. They are best avoided, or worked around by using contains instead of eq.

Replacement tags

Every field or value that you refer to in your smd_if tag becomes available within the containing block so you can display its contents if you wish. Most of the time this is not much use but it can be very useful with the in operator or the :LEN modifier. For instance, if you have asked smd_if to test the URL variable named ‘level’ and told it to compare it to the custom field labelled ‘allowable_levels’, two tags become available which you can use within the containing block:

  • {smd_if_level} would display the value of the ‘level’ URL variable.
  • {smd_if_allowable_levels} would display the contents of the current article’s custom field.

By default the replacement tags are prefixed with smd_if_ so they don’t clash with the ones in smd_gallery (for example, when using smd_if inside an smd_gallery tag). You can change this prefix with the var_prefix attribute.

If you are comparing a fixed-value field (such as field="NULL" or value="12" or smd_gallery’s value="{category}") the name of the replacement tags are {smd_if_fieldN} for fields and {smd_if_valN} for values, where N is the test number starting from 1.

If you use the multiple value options such as in, between, range, etc you will also see replacement tags of the following format: {smd_if_valN_X} where N is the value counter (as above) and X is an incrementing number; with one for each value in your list. For example, value="10/20/30" sets:

  • { smd_if_val1_1 } = 10
  • { smd_if_val1_2 } = 20
  • { smd_if_val1_3 } = 30

There are also ‘length’ replacement tags. Following a similar convention to above, these are prefixed with smd_if_len_. If you get stuck, temporarily switch debug="1" on to see the replacements available and their associated names/values.

See Example 8 and 9 for more.


All user input is tainted by default.

Any time you rely on someone to enter something, at least one person will invariably catch you out; either accidentally or maliciously. For this reason, smd_if supports powerful filtering rules so you can trap and remove suspect input. It already does some of this for you with the NOTAGS modifier, but filtering gives another level of control above that.

Let’s say you are asking the user to enter a value and you need to compare it to a range. What if, instead of a number, they entered fr3d or ;rm -rf 1*;. The plugin might just fall over (probably with an error, which is not very pleasant for users), or it might cause harm to the filesystem if a person trying to hack your site was skilled enough (it’s unlikely, but possible).

In both cases, filtering comes to the rescue. You can specify that you expect only digits from 0 to 9 in your fields and can tell the plugin to chop out anything that is not numeric. So in the first case above, all you would see if the user entered ‘fr3d’ would be ‘3’.

Fortunately — and unfortunately — it’s built around regular expressions which are fiendishly powerful but can also be fiendishly tricky to learn if you are not familiar. They are worth learning.

Let’s dive in at the deep end and tell the plugin that, under no circumstances, must we allow any input that is non-numeric:

<txp:variable name="highest" value="100" />
<txp:smd_if field="urlvar:low" operator="ge, le"
     value="1, txpvar:highest" filter="/[^0-9]+/">
   <p>{smd_if_low} is valid and is between
   {smd_if_val1} and {smd_if_highest}.</p>
<txp:else />
   <p>Sorry, the value {smd_if_low} is not within the
     range {smd_if_val1}-{smd_if_highest}.</p>

[ Eagle-eyed people may notice that something similar can be achieved with the :NUM modifier. The difference here is that the replacement variables are also filtered, whereas with :NUM they contain the original (possibly invalid) input ]

Although out of the scope of this documentation, it’s worth just taking a moment to see what the filter is doing:

  • The forward slashes are start and end delimiters and should always be present (unless you know what you’re doing!)
  • The square brackets [] denote a character class, or group of characters. In this case they contain the range of digits 0 to 9
  • The circumflex ^ negates the class (i.e. non-digits)
  • The plus + means ‘one or more of the things I’ve just seen’

So putting it all together, it reads “Find every occurrence of one or more non-digits”. Thus it looks at every field and finds anything non-numeric. Then it replaces whatever it finds with '', i.e. nothing, nada, zip. Effectively, it deletes whatever non-digits it finds and leaves the good stuff (the numbers) behind.

Replacing bad data

For each matching filter there’s an equivalent replace string. By default this is set to replace="" which means “replace whatever you find with nothing”; or in other words “delete everything that matches the filter”. You may elect to replace your filtered data with something else, say, replace="txp". So if someone entered “fr3d” you would see the replacement variable has a value “txp3txp” (the reason you only get one ‘txp’ before the number is due to the expression being greedy and gobbling up as many characters as it can in a group before replacing them. See any regex tutorial for more on this topic).

Under normal circumstances you won’t want to mess with replace as it’ll do what you want with the default ‘delete’ operation.

Filtering options

By default, the plugin only looks at field data. If you wish to change that, use the filter_in attribute.

The plugin also applies the filter to all fields (filter_type="all"). You may wish to only target a filter at url and server vars, in which case you would specify filter_type="urlvar, svrvar". Or maybe you wish to validate the article image field in case someone entered some rogue data there: filter_type="article_image".

Filtering caveat

If you specify filter_on="field, value" it is important to note that the same filter will be applied to each corresponding field and value. If your filter is too strict there’s a chance it may filter every character out of both field and value, thus if your test was for equality the test would return ‘true’. Here’s an example:

<txp:smd_if field="urlvar:comp1" operator="eq"
     value="urlvar:comp2" filter="/[^a-zA-Z]+/"
     filter_on="field, value">
   // I'm NOT necessarily valid

If your user typed in the URL the plugin would do the following:

  1. Filter comp1 and remove all non-letter characters
  2. Filter comp2 and remove all non-letter characters
  3. Look at comp1 (which is now empty) and comp2 (which is also empty), then compare them

You can see what’s going to happen: the test result is going to be ‘true’ because “nothing” does indeed equal “nothing”. So the act of the user entering two nonsensical, completely numeric strings of data has broken your logic.

For this reason, if you are filtering on both field and value, you should perform additional tests to see if either field / value is set at all. This is better:

<txp:smd_if field="urlvar:comp1, urlvar:comp1, urlvar:comp2"
     operator="eq, isused, isused" value="urlvar:comp2"
     filter_on="field, value">
   // I'm now actually valid

So now, if your filter removes everything from both URL vars, it still fails the ‘has the user entered anything at all’ tests because as far as the plugin is concerned, the visitor has submitted rubbish.

Going further

The above examples all use a single filter. You can specify more than one filter and replacement if you wish, just comma-delimit them (unless you’ve overridden the param_delim of course).

When you specify more than one filter, they ignore the filter_type attribute because the filters are applied in order; one per test. If you wish to skip a particular field and not apply a filter, simply leave an empty comma as a placeholder, e.g. filter=", /[^a-zA-Z0-9\-]+/, , /[^0-9]+/" would apply the respective filters to the 2nd and 4th tests only.

Example 1: standard comparison

<txp:smd_if field="section:name, id"
     operator="begins, gt"
     value="lion, 12">
 <p>The lion sleeps tonight</p>
<txp:else />
 <p>Roooooarrrr! *CHOMP*</p>

Checks if the current section name begins with the word “lion” and the article ID is greater than 12. Displays “The lion sleeps tonight” if both conditions are met or displays the text “Roooooarrrr! CHOMP” if not.

Example 2: other types of field

<txp:smd_if field="summary, category1, urlvar:pic"
     operator="isused, eq, isnum"
     value=", animal ," >
  <p>All matched</p>
<txp:else />
 <p>Match failed</p>

Checks if the custom field labelled “summary” has some data in it, checks if category1 equals “animal” and tests if the urlvar pic is numeric (e.g. ?pic=5).

If all these conditions are met the “All matched” message is displayed, else the “Match failed” message is shown. Note that isused and isnum don’t take arguments for value and their positions are held by empty commas (technically the last comma isn’t needed but it helps keep everything neat if you add further tests later on).

Example 3: using ‘or’ logic

<txp:smd_if field="article_image, svrvar:HTTP_USER_AGENT"
     operator="eq, contains"
     value="urlvar:pic, Safari"
 <p>Come into my parlour</p>
<txp:else />
 <p>Not today, thanks</p>

Compares (for equality) the current article image id with the value of the url variable pic and checks if the value of the HTTP_USER_AGENT string contains “Safari”. This example uses the ‘or’ logic, hence if either condition is met the ‘come into my parlour’ message is shown, otherwise the ‘not today’ message is displayed.

Example 4: sub-category testing

<txp:smd_if field="parent:LVL2"
 <txp:article />
<txp:else />
 <p>Not today, thanks</p>

On a category list page, this checks the 2nd sub-category of the tree to see if it equals “mammal”. If it does, the article is displayed; if not, the message is shown instead. Removing the :LVL2 — which means you can also remove the operator parameter to force the comparison to be the default “contains” — checks if the current (global) category is a child of ‘mammal’ at any nesting level.

Move the example into an article or article Form and change the field to parent:CAT1 to see if the article’s category1 matches ‘mammal’ at any level, or use field="parent:CAT1:LVL2" to combine the checks.

Example 5: defined/undefined/isused/isempty

<txp:smd_if field="urlvar:pic, urlvar:page"
     operator="gt:NUM, undefined"
 <p>Yes please</p>
<txp:else />
 <p>Not today, thanks</p>

Tests if the url variable pic is strictly numerically greater than the value in the current article’s article_image field and that the url variable page is missing from the URL address. Compare the outcome of this test with the other operators using the following table when testing the page urlvar:

<div class=“txp-listtables”>
URL defined undefined isused isempty
index.php?page= TRUE FALSE FALSE TRUE
index.php?page=4 TRUE FALSE TRUE FALSE

Example 6: short circuiting the field

Put this inside your plainlinks form and execute a <txp:linklist /> from an article page/form:

<txp:smd_if field="id"
     operator="ge:NUM, le:NUM"
     value="urlvar:min, urlvar:max">
  <txp:linkdesctitle /><br />

That will list only the links that have IDs between the min and max variables specified on the address bar. Notice that the id field is only listed once and each operator is applied to it in turn.

Example 7: alphanumeric testing

<txp:smd_if field="urlvar:product_code"
  <txp:output_form form="show_product" />
<txp:else />
 <p>Invalid product code</p>

Tests to see if the product_code URL variable is alphanumeric and displays a form if so.

Example 8: displaying used values

<txp:smd_if field="urlvar:sort_order"
  <p>Sorting values by {smd_if_sort_order}</p>
  // Do some stuff
<txp:else />
  // Use a default sort, or show an error here

By using the replacement tag {smd_if_sort_order} you have plucked the value from the URL bar and inserted it into the article. Useful when using the in or notin operators because, although you know that the field matched one of the values in your list, you would otherwise not know which one has been given on the address bar. If you specify the debug attribute in tags like these you can more easily see what replacements are available.

Example 9: using phpvar

global $bodyex;
$bodyex = excerpt(array()).body(array());
<txp:smd_if field="phpvar:bodyex"
  <p>You are a big boy at {smd_if_len_bodyex}
     characters long!</p>

If put in an article Form (NOT directly in an article or you’ll get an out of memory error!), this checks the excerpt and body and shows the message if the combined total length is more than 300 characters.

Example 10: between and range

Check that the given URL params day and month are within reasonable tolerance. Note that it’s still possible to specify the 31st February so you may require extra checks in the container.

<txp:smd_if field="urlvar:day, urlvar:month"
     operator="between, between" value="0/32, 0/13">
   // Likely a valid date supplied

This is functionally equivalent, and probably more obvious to anyone else reading the code:

<txp:smd_if field="urlvar:day, urlvar:month"
     operator="range, range" value="1/31, 1/12">
   // Likely a valid date supplied

If you wanted to factor the year in and make sure that nobody used a year less than 1900 or greater than the current year, try this:

global $thisyear;
$thisyear = date("Y");
<txp:smd_if field="urlvar:d, urlvar:m, urlvar:y"
     operator="range, range, range"
     value="1/31, 1/12, 1900/phpvar:thisyear">
   // Likely a valid date supplied

Example 11: reading multiple values from different places

<txp:variable name="ltuae">42</txp:variable>
<txp:smd_if field="urlvar:trigger"
     operator="in" value="3/15/36/txpvar:ltuae/180/?secret">
   <p>You found one of the magic numbers</p>

First of all we set up the Txp variable, then test the URL variable trigger and see if it is one of the numbers listed in the value attribute. Note that we have specified the Txp variable as one of the numbers, and also the contents of the custom field called secret. Essentially, this builds up the value attribute from all the sources and tests the final result. So if secret held the number 94, this smd_if tag checks if trigger is one of 3, 15, 36, 42, 180 or 94 and displays the message if so. If secret instead contained 94/101/248 these three values would also be tested as part of the in operator.

Example 12: item counts

This counts the number of items in my_list and tests them against the value. Note that list_delim is used between list items.

<txp:variable name="my_list">8 / 42 / 11 / 75 / 14</txp:variable>
<txp:smd_if field="txpvar:my_list" operator="gt:COUNT" value="3">
   Yes, there are {smd_if_val1} or more values
   (actually: {smd_if_count_my_list})
<txp:else />
  There are fewer than {smd_if_val1} values in the list.


Stef Dawson. Based on an idea brewing in the back of my mind while hacking chs_if_urlvar.

Source code

If you’d rather frolic in the raw code halls, you’ll need to step into the view source page.

Legacy software

If, for some inexplicable reason, you need an ancient version of a plugin, it can probably be found on the plugin archive page.

Experimental software

If you’re feeling brave, or fancy swimming with piranhas, you can test out some of my beta code. It can be found on the plugin beta page.