Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2577 (2024-01-12 21:38:45)

feat: Add 'sort by function'
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->
Enable `sort by function` to return a sort key, which is then used for sorting tasks in search results.

**Note:** 

- The tests (and hence also behaviour) and docs are not yet up to (my) release standards.
- I'm merging it now so that I can use it in my main vault, to confirm I'm happy with the behaviour, and to try to come up with some more realistic examples for the docs.

## Motivation and Context

Partial implementation of #1479.

## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

- Adding more tests
- Exploratory testing

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [ ] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/docs/Getting Started/Statuses/Status Types.md b/docs/Getting Started/Statuses/Status Types.md
index a1812a13..c5f5446c 100644
--- a/docs/Getting Started/Statuses/Status Types.md
+++ b/docs/Getting Started/Statuses/Status Types.md
@@ -1,5 +1,7 @@
 ---
 publish: true
+aliases:
+  - Status Type
 ---
 
 # Status Types
diff --git a/docs/Introduction.md b/docs/Introduction.md
index 2d2a0b54..3b999876 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -13,6 +13,7 @@ _In recent [releases](https://github.com/obsidian-tasks-group/obsidian-tasks/rel
     Move the older ones down to the top of the comment block below...
 -->
 
+- X.Y.Z: 🔥 Add [[Custom Sorting|custom sorting]].
 - 5.6.0: 🔥 The [[Postponing|postpone]] menu now offers `today` and `tomorrow`.
 - 5.5.0: 🔥 The [[Create or edit Task]] modal can now edit Created, Done and Cancelled dates
 - 5.5.0: 🔥 Add support for [[Dates#Cancelled date|cancelled dates]].
diff --git a/docs/Queries/Sorting.md b/docs/Queries/Sorting.md
index 7dfbd176..a31076e0 100644
--- a/docs/Queries/Sorting.md
+++ b/docs/Queries/Sorting.md
@@ -4,6 +4,8 @@ publish: true
 
 # Sorting
 
+<span class="related-pages">#feature/sorting</span>
+
 <!-- NEW_QUERY_INSTRUCTION_EDIT_REQUIRED -->
 
 ## Contents
@@ -26,6 +28,10 @@ By default Tasks sorts tasks by [[Urgency|a calculated score we call "urgency"]]
 
 To sort the results of a query different from the default, you must add at least one `sort by` line to the query.
 
+## Custom Sorting
+
+==TODO Populate this section==
+
 ## Sort by Task Statuses
 
 For more information, including adding your own customised statuses, see [[Statuses]].
@@ -34,6 +40,18 @@ For more information, including adding your own customised statuses, see [[Statu
 
 - `sort by status` (done or todo)
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by status** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.isDone_docs.approved.md -->
+
+```javascript
+sort by function task.isDone
+```
+
+- Tasks with [[Status Types|Status Type]] `TODO` and `IN_PROGRESS` tasks are sorted before those with types `DONE`, `CANCELLED` and `NON_TASK.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Status Name
 
 - `sort by status.name` (Done, Todo, Cancelled, In Progress, Unknown, My very important custom status, etc - sorted alphabetically)
@@ -41,6 +59,18 @@ For more information, including adding your own customised statuses, see [[Statu
 > [!released]
 `sort by status.name` was introduced in Tasks 1.23.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by status names** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.statuses_task.status.name_docs.approved.md -->
+
+```javascript
+sort by function task.status.name
+```
+
+- Identical to "Sort by status.name".
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Status Type
 
 - `sort by status.type` (Sorted in the order `IN_PROGRESS`, `TODO`, `DONE`, `CANCELLED` then `NON_TASK`)
@@ -48,24 +78,120 @@ For more information, including adding your own customised statuses, see [[Statu
 > [!released]
 `sort by status.type` was introduced in Tasks 1.23.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by status types** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.statuses_task.status.type_docs.approved.md -->
+
+```javascript
+sort by function task.status.type
+```
+
+- Unlike "Sort by status.type", this sorts the status types in alphabetical order.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+### Status Symbol
+
+There is no built-in instruction to sort by status symbols.
+
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by status symbol** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.statuses_task.status.symbol_docs.approved.md -->
+
+```javascript
+sort by function task.status.symbol
+```
+
+- Sort by the status symbol.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+### Next Status Symbol
+
+There is no built-in instruction to sort by next status symbols.
+
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by next status symbol** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.statuses_task.status.nextSymbol_docs.approved.md -->
+
+```javascript
+sort by function task.status.nextSymbol
+```
+
+- Sort by the next status symbol.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ## Sort by Dates in Tasks
 
 ### Done Date
 
 - `sort by done` (the date when the task was done)
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by done date** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.done_docs.approved.md -->
+
+```javascript
+sort by function task.done.format("dddd")
+```
+
+- Sort by done date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Due Date
 
 - `sort by due` (the date when the task is due)
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by due date** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.due_docs.approved.md -->
+
+```javascript
+sort by function task.due.format("dddd")
+```
+
+- Sort by due date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.due.advanced_docs.approved.md -->
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Scheduled Date
 
 - `sort by scheduled` (the date when the task is scheduled)
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by scheduled date** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.scheduled_docs.approved.md -->
+
+```javascript
+sort by function task.scheduled.format("dddd")
+```
+
+- Sort by scheduled date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Start Date
 
 - `sort by start` (the date when the task starts)
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by start date** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.start_docs.approved.md -->
+
+```javascript
+sort by function task.start.format("dddd")
+```
+
+- Sort by start date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Created Date
 
 - `sort by created` (the date when the task was created)
@@ -73,6 +199,18 @@ For more information, including adding your own customised statuses, see [[Statu
 > [!released]
 `sort by created` was introduced in Tasks 2.0.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by created date** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.created_docs.approved.md -->
+
+```javascript
+sort by function task.created.format("dddd")
+```
+
+- Sort by created date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Cancelled Date
 
 - `sort by cancelled` (the date when the task was cancelled)
@@ -80,6 +218,18 @@ For more information, including adding your own customised statuses, see [[Statu
 > [!released]
 `sort by cancelled` was introduced in Tasks 5.5.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by cancelled date** is now possible, using `task.cancelled`.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.cancelled_docs.approved.md -->
+
+```javascript
+sort by function task.cancelled.format("dddd")
+```
+
+- Sort by cancelled date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Happens
 
 - `sort by happens` (the earliest of start date, scheduled date, and due date)
@@ -87,24 +237,122 @@ For more information, including adding your own customised statuses, see [[Statu
 > [!released]
 `sort by happens` was introduced in Tasks 1.21.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by happens date** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.happens_docs.approved.md -->
+
+```javascript
+sort by function task.happens.format("dddd")
+```
+
+- Sort by happens date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ## Sort by Other Task Properties
 
 ### Description
 
 - `sort by description` (the description of the task)
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by description** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.description_docs.approved.md -->
+
+```javascript
+sort by function task.description.length
+```
+
+- Sort by length of description, shortest first.
+- This might be useful for finding tasks that need more information, or could be made less verbose.
+
+```javascript
+sort by function task.description.replace('🟥', 1).replace('🟧', 2).replace('🟨', 3).replace('🟩', 4).replace('🟦', 5)
+```
+
+- A user has defined custom system for their task descriptions, with coloured squares at the **start** of task lines as a home-grown alternative priority system.
+- This allows tasks to be sorted in the order of their coloured squares.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+### Description without tags
+
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by description with tags removed** is now possible.
+
+The value `task.descriptionWithoutTags` returns a copy of the description with all the tags removed, so that you can sort together any tasks whose descriptions differ only by their tags.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.descriptionWithoutTags_docs.approved.md -->
+
+```javascript
+sort by function task.descriptionWithoutTags
+```
+
+- Like `Sort by description`, but it removes any tags from the sort key.
+- This might be useful for sorting together completed recurrences of the same task, even if the tags differ in some recurrences.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Priority
 
 - `sort by priority` (priority of the task; "low" is below "none": [[Priority|priorities]])
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by priority name and number** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.priorityName_docs.approved.md -->
+
+```javascript
+sort by function task.priorityName
+```
+
+- Sort by the task's priority name.
+- The priority names are displayed in alphabetical order.
+- Note that the default priority is called 'Normal', as opposed to with `Sort by priority` which calls the default 'None'.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.priorityNumber_docs.approved.md -->
+
+```javascript
+sort by function task.priorityNumber
+```
+
+- Sort by the task's priority number, where Highest is 0 and Lowest is 5.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Urgency
 
 - `sort by urgency` ([[Urgency|urgency]])
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by urgency** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.urgency_docs.approved.md -->
+
+```javascript
+sort by function reverse task.urgency
+```
+
+- Sort by task urgency values.
+- We use `reverse` to put the most urgent tasks first.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Recurrence
 
 - `sort by recurring` (recurring tasks sort before non-recurring ones: [[Recurring Tasks]])
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by recurrence** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.isRecurring_docs.approved.md -->
+
+```javascript
+sort by function task.isRecurring
+```
+
+- Sort by whether the task is recurring.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Tags
 
 See [[Tags]] for important information about how tags behave in the Tasks plugin.
@@ -122,19 +370,109 @@ For example this query will sort by the second tag found in the description.
 > [!released]
 Tag sorting was introduced in Tasks 1.6.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by tags** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.tags_docs.approved.md -->
+
+```javascript
+sort by function task.tags.filter( (tag) => tag.includes("#context/")).sort().join(",")
+```
+
+- Sort by tags that contain "#context/".
+- Any tasks without that tag are sorted first.
+
+```javascript
+sort by function reverse task.tags.length
+```
+
+- Sort by the number of tags on the task.
+- The `reverse` option puts tasks with the most tags first.
+
+```javascript
+sort by function -task.tags.length
+```
+
+- A different way of sorting by the number of tags on the task, still putting tasks with the most tags first.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.tags.advanced_docs.approved.md -->
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+### Original Markdown
+
+There is no built-in instruction to sort by the original markdown line.
+
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by original markdown line** is now possible.
+
+For example, this could be used to extract information from `task.originalMarkdown` that Tasks does not parse, to use for sorting tasks.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.originalMarkdown_docs.approved.md -->
+
+```javascript
+sort by function task.originalMarkdown
+```
+
+- Sort by the raw text of the task's original line in the MarkDown file.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ## Sort by File Properties
 
 ### File Path
 
 - `sort by path` (the path to the file that contains the task)
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by file path** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.file_properties_task.file.path_docs.approved.md -->
+
+```javascript
+sort by function task.file.path
+```
+
+- Like 'Sort by path' but includes the file extension.
+
+```javascript
+sort by function task.file.pathWithoutExtension
+```
+
+- Like 'Sort by path'.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Root
 
-It is not currently possible to sort by the top-level folder that contains the task.
+There is no built-in instruction to sort by the top-level folder that contains the task.
+
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by root folder** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.file_properties_task.file.root_docs.approved.md -->
+
+```javascript
+sort by function task.file.root
+```
+
+- Enable sorting by the root folder.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
 
 ### Folder
 
-It is not currently possible to sort by the folder that contains the task.
+There is no built-in instruction to sort by the folder that contains the task.
+
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by folder** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.file_properties_task.file.folder_docs.approved.md -->
+
+```javascript
+sort by function task.file.folder
+```
+
+- Enable sorting by the folder containing the task.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
 
 ### File Name
 
@@ -144,6 +482,24 @@ It is not currently possible to sort by the folder that contains the task.
 > [!released]
 `sort by filename` was introduced in Tasks 1.21.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by file name** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.file_properties_task.file.filename_docs.approved.md -->
+
+```javascript
+sort by function task.file.filename
+```
+
+- Like 'sort by filename' but includes the file extension.
+
+```javascript
+sort by function task.file.filenameWithoutExtension
+```
+
+- Like 'sort by filename'.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ### Heading
 
 - `sort by sort by heading` (the heading preceding the task; files with empty headings sort before other tasks)
@@ -151,6 +507,18 @@ It is not currently possible to sort by the folder that contains the task.
 > [!released]
 `sort by heading` was introduced in Tasks 1.21.0.
 
+Since Tasks X.Y.Z, **[[Custom Sorting|custom sorting]] by heading** is now possible.
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.file_properties_task.heading_docs.approved.md -->
+
+```javascript
+sort by function task.heading
+```
+
+- Like 'sort by heading'.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
 ## Multiple sort criteria
 
 You can add multiple `sort by` query options, each on an extra line.
diff --git a/docs/Scripting/About Scripting.md b/docs/Scripting/About Scripting.md
index 0d374185..8bfc5a23 100644
--- a/docs/Scripting/About Scripting.md
+++ b/docs/Scripting/About Scripting.md
@@ -23,6 +23,8 @@ We are using the word 'scripting' in a very loose sense here:
 
 - [[Custom Filters]] - write short JavaScript expressions to create task search filters.
   - See also the many `filter by function` examples added to the [[Filters]] documentation.
+- [[Custom Sorting]] - write short JavaScript expressions to sort tasks in Tasks query results.
+  - See also the many `sort by function` examples added to the [[Sorting]] documentation.
 - [[Custom Grouping]] - write short JavaScript expressions to create task group names in Tasks query results.
   - See also the many `group by function` examples added to the [[Grouping]] documentation.
 
diff --git a/docs/Scripting/Custom Sorting.md b/docs/Scripting/Custom Sorting.md
new file mode 100644
index 00000000..757de142
--- /dev/null
+++ b/docs/Scripting/Custom Sorting.md
@@ -0,0 +1,171 @@
+---
+publish: true
+---
+
+# Custom Sorting
+
+<span class="related-pages">#feature/scripting #feature/sorting</span>
+
+> [!released]
+> Custom sorting was introduced in Tasks X.Y.Z.
+
+## Summary
+
+- Define your own custom task sort order, using JavaScript expressions such as:
+  - `sort by function task.description.length`
+- There are examples in [[Sorting]].
+  - Search for `sort by function` in that file.
+- Find all the **supported tasks properties** in [[Task Properties]] and [[Quick Reference]].
+  - A number of properties are only available for custom sorting, and not for built-in sorting instructions.
+- Find all the **supported query properties** in [[Query Properties]].
+  - ==TODO Query properties are not yet supported in custom sorters.==
+- Learn a bit about how expressions work in [[Expressions]].
+
+## Custom sorting introduction
+
+The Tasks plugin provides a lot of built-in ways to [[Sorting|sort]] similar tasks in Tasks query results.
+
+But sometimes the built-in facility just doesn't quite do what you want.
+
+**Custom sorting** allows you to **invent your own ordering scheme** to sort tasks.
+
+You use the instruction `sort by function` and then add a rule, written in JavaScript, to calculate a sort key for a task. See the examples below.
+
+## How it works
+
+### Available Task Properties
+
+The Reference section [[Task Properties]] shows all the task properties available for use in custom sorting.
+
+The available task properties are also shown in the [[Quick Reference]] table.
+### Available Query Properties
+
+The Reference section [[Query Properties]] shows all the query properties available for use via [[Placeholders]] in custom sorting.
+
+Any placeholders in custom sorts must be surrounded by quotes.
+
+> [!released]
+> Query properties and placeholders were introduced in Tasks 4.7.0.
+
+### Expressions
+
+The instructions look like this:
+
+- `sort by function <expression>`
+- `sort by function reverse <expression>`
+
+The expression is evaluated (calculated) on each task that matches your query, and the expression result is used as the sort key for the task.
+
+==TODO Update this==
+
+| Desired heading                                                             | Values that you can return                                                                             |
+| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
+| A single sort name for the task                                            | A single value, such as `'sort name'`.<br>An array, with a single value in, such as `['sort name']`. |
+| Display the task potentially more than once (as is done by `sort by tags`) | An array of values, such as:<br>`['heading 1', 'heading 2']`                                             |
+| No heading                                                                  | `null`<br>Empty string `''`<br>Empty array `[]`                                                        |
+
+The `expression` can:
+
+- use a large range of properties of each task
+- use any valid JavaScript language features
+
+The `expression` must:
+
+==TODO Update this==
+
+- use properties on a given task, such as `task.description`, `task.status.name`
+  - See the reference page [[Task Properties]] for all the available properties
+- return one of:
+  - either a single value of any type that can be converted to string
+  - or an array of values (in which case, the task will be displayed multiple times, once under each heading generated from the array)
+
+## Example custom sorts
+
+Below are some examples to give a flavour of what can be done with custom sorts.
+
+You can find many more examples by searching for `sort by function` in the [[Sorting]] page.
+
+### Text property examples
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.description_docs.approved.md -->
+
+```javascript
+sort by function task.description.length
+```
+
+- Sort by length of description, shortest first.
+- This might be useful for finding tasks that need more information, or could be made less verbose.
+
+```javascript
+sort by function task.description.replace('🟥', 1).replace('🟧', 2).replace('🟨', 3).replace('🟩', 4).replace('🟦', 5)
+```
+
+- A user has defined custom system for their task descriptions, with coloured squares at the **start** of task lines as a home-grown alternative priority system.
+- This allows tasks to be sorted in the order of their coloured squares.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+### Date property examples
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.dates_task.due_docs.approved.md -->
+
+```javascript
+sort by function task.due.format("dddd")
+```
+
+- Sort by due date's day of the week, alphabetically.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+### Number property examples
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.other_properties_task.urgency_docs.approved.md -->
+
+```javascript
+sort by function reverse task.urgency
+```
+
+- Sort by task urgency values.
+- We use `reverse` to put the most urgent tasks first.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+### File property examples
+
+<!-- placeholder to force blank line before included text --><!-- include: CustomSortingExamples.test.file_properties_task.file.folder_docs.approved.md -->
+
+```javascript
+sort by function task.file.folder
+```
+
+- Enable sorting by the folder containing the task.
+
+<!-- placeholder to force blank line after included text --><!-- endInclude -->
+
+## Troubleshooting
+
+> [!Warning]
+> Currently most types of error in function expressions are only detected when the search runs.
+>
+> This means that error messages are displayed in the sort headings, when results are viewed.
+>
+> In a future release, we plan to show errors in formulae when the query block is being read.
+
+### Syntax error
+
+The following example gives an error:
+
+````text
+```tasks
+sort by function hello
+```
+````
+
+gives this heading name:
+
+```text
+##### Error: Failed calculating expression "hello". The error message was: hello is not defined
+```
+
+> [!todo]
+> Do syntax-error checking when parsing the instruction
diff --git a/mdsnippets.json b/mdsnippets.json
index 0fbd3754..a8c4ae04 100644
--- a/mdsnippets.json
+++ b/mdsnippets.json
@@ -1,6 +1,6 @@
 {
   "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json",
-  "ExcludeDirectories": [ "node_modules", "contributing/_meta", "docs/_meta", "coverage" ],
+  "ExcludeDirectories": [ "node_modules", "contributing/_meta", "docs/_meta", "coverage", "venv" ],
   "Convention": "InPlaceOverwrite",
   "TocLevel": 5,
   "OmitSnippetLinks": true,
diff --git a/src/Query/Filter/FunctionField.ts b/src/Query/Filter/FunctionField.ts
index eac119ee..e274cdac 100644
--- a/src/Query/Filter/FunctionField.ts
+++ b/src/Query/Filter/FunctionField.ts
@@ -5,6 +5,7 @@ import { Explanation } from '../Explain/Explanation';
 import { TaskExpression, parseAndEvaluateExpression } from '../../Scripting/TaskExpression';
 import type { QueryContext } from '../../Scripting/QueryContext';
 import type { SearchInfo } from '../SearchInfo';
+import { Sorter } from '../Sorter';
 import { Field } from './Field';
 import { Filter, type FilterFunction } from './Filter';
 import { FilterOrErrorMessage } from './FilterOrErrorMessage';
@@ -15,6 +16,10 @@ import { FilterOrErrorMessage } from './FilterOrErrorMessage';
  * See also {@link parseAndEvaluateExpression}
  */
 export class FunctionField extends Field {
+    // -----------------------------------------------------------------------------------------------------------------
+    // Filtering
+    // -----------------------------------------------------------------------------------------------------------------
+
     createFilterOrErrorMessage(line: string): FilterOrErrorMessage {
         const match = Field.getMatch(this.filterRegExp(), line);
         if (match === null) {
@@ -40,6 +45,50 @@ export class FunctionField extends Field {
         return new RegExp(`^filter by ${this.fieldNameSingularEscaped()} (.*)`, 'i');
     }
 
+    // -----------------------------------------------------------------------------------------------------------------
+    // Sorting
+    // -----------------------------------------------------------------------------------------------------------------
+
+    public supportsSorting(): boolean {
+        return true;
+    }
+
+    protected sorterRegExp(): RegExp {
+        return new RegExp(`^sort by ${this.fieldNameSingularEscaped()}( reverse)? (.*)`, 'i');
+    }
+
+    public createSorterFromLine(line: string): Sorter | null {
+        const match = Field.getMatch(this.sorterRegExp(), line);
+        if (match === null) {
+            return null;
+        }
+
+        const reverse = !!match[1];
+        const expression = match[2];
+        const taskExpression = new TaskExpression(expression);
+        if (!taskExpression.isValid()) {
+            // TODO Figure out error handling
+            // return FilterOrErrorMessage.fromError(line, taskExpression.parseError!);
+            return null;
+        }
+        const comparator = (a: Task, b: Task) => {
+            // If the result is negative, a is sorted before b.
+            // If the result is positive, b is sorted before a.
+            // If the result is 0, no changes are done with the sort order of the two values.
+
+            const valueA = taskExpression.evaluate(a);
+            const valueB = taskExpression.evaluate(b);
+
+            if (typeof valueA === 'string') {
+                return valueA.localeCompare(valueB, undefined, { numeric: true });
+            }
+
+            // Treat as numeric
+            return valueA - valueB;
+        };
+        return new Sorter(line, this.fieldNameSingular(), comparator, reverse);
+    }
+
     // -----------------------------------------------------------------------------------------------------------------
     // Grouping
     // -----------------------------------------------------------------------------------------------------------------


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 83889f6d..b0fda805 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -242,6 +242,8 @@ describe('Query parsing', () => {
             'sort by due reverse',
             'sort by filename',
             'sort by filename reverse',
+            'sort by function reverse task.description.length',
+            'sort by function task.description.length',
             'sort by happens',
             'sort by happens reverse',
             'sort by heading',
diff --git a/tests/Query/Filter/FunctionField.test.ts b/tests/Query/Filter/FunctionField.test.ts
index af9ba7c0..97fd6a83 100644
--- a/tests/Query/Filter/FunctionField.test.ts
+++ b/tests/Query/Filter/FunctionField.test.ts
@@ -5,6 +5,7 @@ import moment from 'moment';
 
 import { FunctionField } from '../../../src/Query/Filter/FunctionField';
 import { Status } from '../../../src/Status';
+import type { Task } from '../../../src/Task';
 import { Priority } from '../../../src/Task';
 import {
     toGroupTaskFromBuilder,
@@ -13,7 +14,12 @@ import {
 } from '../../CustomMatchers/CustomMatchersForGrouping';
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
 import { SearchInfo } from '../../../src/Query/SearchInfo';
-import type { Task } from '../../../src/Task';
+import {
+    expectTaskComparesAfter,
+    expectTaskComparesBefore,
+    expectTaskComparesEqual,
+} from '../../CustomMatchers/CustomMatchersForSorting';
+import { fromLine } from '../../TestHelpers';
 
 window.moment = moment;
 
@@ -82,9 +88,59 @@ describe('FunctionField - filtering', () => {
 // -----------------------------------------------------------------------------------------------------------------
 
 describe('FunctionField - sorting', () => {
-    it('should not support sorting', () => {
+    it('should support sorting', () => {
         const functionField = new FunctionField();
-        expect(functionField.supportsSorting()).toEqual(false);
+        expect(functionField.supportsSorting()).toEqual(true);
+    });
+
+    // Helper function to create a task with a given path
+    function with_description(description: string) {
+        return new TaskBuilder().description(description).build();
+    }
+
+    it('sort by function - integer expression', () => {
+        // Arrange
+        const sorter = new FunctionField().createSorterFromLine('sort by function task.description.length');
+
+        // Assert
+        expect(sorter).not.toBeNull();
+        expectTaskComparesEqual(sorter!, with_description('Aaa'), with_description('Aaa'));
+        expectTaskComparesEqual(sorter!, with_description('AAA'), with_description('ZZZ'));
+
+        // Sorts on string length - shorter first
+        expectTaskComparesBefore(sorter!, with_description('AAA'), with_description('AAAA'));
+        expectTaskComparesBefore(sorter!, with_description(''), with_description('B'));
+
+        // Sorts on string length - longer first
+        expectTaskComparesAfter(sorter!, with_description('xxxx'), with_description('x'));
+    });
+
+    it('sort by function - integer expression - reverse', () => {
+        // Arrange
+        const sorter = new FunctionField().createSorterFromLine('sort by function reverse task.description.length');
+
+        // Assert
+        expect(sorter).not.toBeNull();
+        expectTaskComparesEqual(sorter!, with_description('Aaa'), with_description('Aaa'));
+
+        // Sorts on string length - shorter first
+        expectTaskComparesAfter(sorter!, with_description('AAA'), with_description('AAAA'));
+
+        // Sorts on string length - longer first
+        expectTaskComparesBefore(sorter!, with_description('xxxx'), with_description('x'));
+    });
+
+    it('sort by function - string expression', () => {
+        // Arrange
+        const sorter = new FunctionField().createSorterFromLine('sort by function task.originalMarkdown');
+
+        // Assert
+        expect(sorter).not.toBeNull();
+        expectTaskComparesBefore(sorter!, fromLine({ line: '- [ ] Hello' }), fromLine({ line: '* [ ] Hello' }));
+        expectTaskComparesBefore(sorter!, fromLine({ line: '* [ ] Apple' }), fromLine({ line: '* [ ] Hello' }));
+        expectTaskComparesBefore(sorter!, fromLine({ line: '* [ ] Apple' }), fromLine({ line: '* [ ] APPLE' })); // case-insensitive
+        expectTaskComparesEqual(sorter!, fromLine({ line: '- [ ] Hello' }), fromLine({ line: '- [ ] Hello' }));
+        expectTaskComparesAfter(sorter!, fromLine({ line: '* [ ] Hello' }), fromLine({ line: '- [ ] Hello' }));
     });
 });
 
diff --git a/tests/Query/Filter/StatusNameField.test.ts b/tests/Query/Filter/StatusNameField.test.ts
index b628faca..6b79d601 100644
--- a/tests/Query/Filter/StatusNameField.test.ts
+++ b/tests/Query/Filter/StatusNameField.test.ts
@@ -102,6 +102,6 @@ describe('grouping by status.name', () => {
         const grouper = new StatusNameField().createNormalGrouper();
         const tasks = SampleTasks.withAllStatuses();
 
-        expect({ grouper, tasks }).groupHeadingsToBe(['Cancelled', 'Done', 'EMPTY', 'In Progress', 'Todo']);
+        expect({ grouper, tasks }).groupHeadingsToBe(['Cancelled', 'Done', 'EMPTY', 'In Progress', 'Non-Task', 'Todo']);
     });
 });
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.other_properties_task.isDone_results.approved.txt b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.other_properties_task.isDone_results.approved.txt
index dab86d4e..d1834b25 100644
--- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.other_properties_task.isDone_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.other_properties_task.isDone_results.approved.txt
@@ -7,6 +7,7 @@ Same as the `done` filter, but might be useful in conjunction with other express
 =>
 - [-] Status Cancelled
 - [x] Status Done
+- [Q] Status Non-Task
 ====================================================================================
 
 
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.symbol_results.approved.txt b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.symbol_results.approved.txt
index f786bb41..49f677fd 100644
--- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.symbol_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.symbol_results.approved.txt
@@ -16,6 +16,7 @@ Find tasks with anything but the space character as their status symbol, that is
 - [x] Status Done
 - [] Status EMPTY
 - [/] Status In Progress
+- [Q] Status Non-Task
 - [p] Unknown symbol
 - [s] Toggles to self
 - [P] Pro
@@ -32,6 +33,7 @@ Note that because we use a variable to avoid repetition, we need to add `return`
 Find tasks with status symbol `P`, `C`, `Q` or `A`.
 This can get quite verbose, the more symbols you want to search for.
 =>
+- [Q] Status Non-Task
 - [P] Pro
 - [C] Con
 - [Q] Question
@@ -44,6 +46,7 @@ Find tasks with status symbol `P`, `C`, `Q` or `A`.
 This is a convenient shortcut over a longer statement testing each allowed value independently.
 =>
 - [] Status EMPTY
+- [Q] Status Non-Task
 - [P] Pro
 - [C] Con
 - [Q] Question
@@ -54,6 +57,7 @@ This is a convenient shortcut over a longer statement testing each allowed value
 filter by function !' -x/'.includes(task.status.symbol)
 Find tasks with any status symbol not supported by Tasks in the default settings.
 =>
+- [Q] Status Non-Task
 - [p] Unknown symbol
 - [s] Toggles to self
 - [P] Pro
diff --git a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.type_results.approved.txt b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.type_results.approved.txt
index 6ffec625..9def5932 100644
--- a/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.type_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomFiltering/CustomFilteringExamples.test.statuses_task.status.type_results.approved.txt
@@ -5,7 +5,7 @@ Results of custom filters
 filter by function task.status.type === 'NON_TASK'
 Find tasks of type `NON_TASK`.
 =>
-
+- [Q] Status Non-Task
 ====================================================================================
 
 
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.name_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.name_results.approved.txt
index dc8b7163..bc4333c4 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.name_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.name_results.approved.txt
@@ -9,6 +9,7 @@ Cancelled
 Done
 EMPTY
 In Progress
+Non-Task
 Todo
 ====================================================================================
 
@@ -20,6 +21,7 @@ CANCELLED
 DONE
 EMPTY
 IN PROGRESS
+NON-TASK
 TODO
 ====================================================================================
 
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.nextSymbol_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.nextSymbol_results.approved.txt
index f17e17a5..fe85dfe4 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.nextSymbol_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.nextSymbol_results.approved.txt
@@ -6,6 +6,7 @@ group by function "Next status symbol: " + task.status.nextSymbol.replace(" ", "
 Group by the next status symbol, making space characters visible.
 =>
 Next status symbol: 
+Next status symbol: A
 Next status symbol: space
 Next status symbol: x
 ====================================================================================
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.symbol_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.symbol_results.approved.txt
index 8df43a0b..e30559b7 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.symbol_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.symbol_results.approved.txt
@@ -8,6 +8,7 @@ Group by the status symbol, making space characters visible.
 Status symbol: 
 Status symbol: -
 Status symbol: /
+Status symbol: Q
 Status symbol: space
 Status symbol: x
 ====================================================================================
diff --git a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.type_results.approved.txt b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.type_results.approved.txt
index d04dc56a..a3ae34c2 100644
--- a/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.type_results.approved.txt
+++ b/tests/Scripting/ScriptingReference/CustomGrouping/CustomGroupingExamples.test.statuses_task.status.type_results.approved.txt
@@ -9,6 +9,7 @@ CANCELLED
 DONE
 EMPTY
 IN_PROGRESS
+NON_TASK
 TODO
 ====================================================================================
 
@@ -20,6 +21,7 @@ This sorts the status types in the same order as "group by status.type".
 %%2%%TODO
 %%3%%DONE
 %%4%%CANCELLED
+%%5%%NON_TASK
 %%6%%EMPTY
 ====================================================================================
 
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.cancelled_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.cancelled_docs.approved.md
new file mode 100644
index 00000000..32918835
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.cancelled_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.cancelled.format("dddd")
+```
+
+- Sort by cancelled date's day of the week, alphabetically.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.cancelled_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.cancelled_results.approved.txt
new file mode 100644
index 00000000..3deead86
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.cancelled_results.approved.txt
@@ -0,0 +1,14 @@
+Results of custom sorters
+
+
+
+sort by function task.cancelled.format("dddd")
+Sort by cancelled date's day of the week, alphabetically.
+=>
+- [ ] my description
+- [ ] my description ❌ Invalid date
+- [ ] my description ❌ 2023-06-01
+- [ ] my description ❌ 2023-05-30
+- [ ] my description ❌ 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.created_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.created_docs.approved.md
new file mode 100644
index 00000000..ed967d2c
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.created_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.created.format("dddd")
+```
+
+- Sort by created date's day of the week, alphabetically.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.created_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.created_results.approved.txt
new file mode 100644
index 00000000..d56f3c46
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.created_results.approved.txt
@@ -0,0 +1,14 @@
+Results of custom sorters
+
+
+
+sort by function task.created.format("dddd")
+Sort by created date's day of the week, alphabetically.
+=>
+- [ ] my description
+- [ ] my description ➕ Invalid date
+- [ ] my description ➕ 2023-06-01
+- [ ] my description ➕ 2023-05-30
+- [ ] my description ➕ 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.done_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.done_docs.approved.md
new file mode 100644
index 00000000..261fc592
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.done_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.done.format("dddd")
+```
+
+- Sort by done date's day of the week, alphabetically.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.done_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.done_results.approved.txt
new file mode 100644
index 00000000..3d9ef988
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.done_results.approved.txt
@@ -0,0 +1,14 @@
+Results of custom sorters
+
+
+
+sort by function task.done.format("dddd")
+Sort by done date's day of the week, alphabetically.
+=>
+- [ ] my description
+- [ ] my description ✅ Invalid date
+- [ ] my description ✅ 2023-06-01
+- [ ] my description ✅ 2023-05-30
+- [ ] my description ✅ 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due.advanced_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due.advanced_docs.approved.md
new file mode 100644
index 00000000..ee400937
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due.advanced_docs.approved.md
@@ -0,0 +1,5 @@
+<!-- placeholder to force blank line before included text -->
+
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due_docs.approved.md
new file mode 100644
index 00000000..e84275fa
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.due.format("dddd")
+```
+
+- Sort by due date's day of the week, alphabetically.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due_results.approved.txt
new file mode 100644
index 00000000..d9e9e47a
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.due_results.approved.txt
@@ -0,0 +1,14 @@
+Results of custom sorters
+
+
+
+sort by function task.due.format("dddd")
+Sort by due date's day of the week, alphabetically.
+=>
+- [ ] my description
+- [ ] my description 📅 Invalid date
+- [ ] my description 📅 2023-06-01
+- [ ] my description 📅 2023-05-30
+- [ ] my description 📅 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.happens_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.happens_docs.approved.md
new file mode 100644
index 00000000..2577d121
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.happens_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.happens.format("dddd")
+```
+
+- Sort by happens date's day of the week, alphabetically.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.happens_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.happens_results.approved.txt
new file mode 100644
index 00000000..9f0a6fdd
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.happens_results.approved.txt
@@ -0,0 +1,14 @@
+Results of custom sorters
+
+
+
+sort by function task.happens.format("dddd")
+Sort by happens date's day of the week, alphabetically.
+=>
+- [ ] my description
+- [ ] my description 📅 Invalid date
+- [ ] my description 📅 2023-06-01
+- [ ] my description 📅 2023-05-30
+- [ ] my description 📅 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.scheduled_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.scheduled_docs.approved.md
new file mode 100644
index 00000000..270b5d70
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.scheduled_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.scheduled.format("dddd")
+```
+
+- Sort by scheduled date's day of the week, alphabetically.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.scheduled_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.scheduled_results.approved.txt
new file mode 100644
index 00000000..141d2ef1
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.scheduled_results.approved.txt
@@ -0,0 +1,14 @@
+Results of custom sorters
+
+
+
+sort by function task.scheduled.format("dddd")
+Sort by scheduled date's day of the week, alphabetically.
+=>
+- [ ] my description
+- [ ] my description ⏳ Invalid date
+- [ ] my description ⏳ 2023-06-01
+- [ ] my description ⏳ 2023-05-30
+- [ ] my description ⏳ 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.start_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.start_docs.approved.md
new file mode 100644
index 00000000..52489d76
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.start_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.start.format("dddd")
+```
+
+- Sort by start date's day of the week, alphabetically.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.start_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.start_results.approved.txt
new file mode 100644
index 00000000..d7940d7f
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.dates_task.start_results.approved.txt
@@ -0,0 +1,14 @@
+Results of custom sorters
+
+
+
+sort by function task.start.format("dddd")
+Sort by start date's day of the week, alphabetically.
+=>
+- [ ] my description
+- [ ] my description 🛫 Invalid date
+- [ ] my description 🛫 2023-06-01
+- [ ] my description 🛫 2023-05-30
+- [ ] my description 🛫 2023-05-31
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.filename_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.filename_docs.approved.md
new file mode 100644
index 00000000..1f502d7a
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.filename_docs.approved.md
@@ -0,0 +1,17 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.file.filename
+```
+
+- Like 'sort by filename' but includes the file extension.
+
+```javascript
+sort by function task.file.filenameWithoutExtension
+```
+
+- Like 'sort by filename'.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.filename_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.filename_results.approved.txt
new file mode 100644
index 00000000..26325c7f
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.filename_results.approved.txt
@@ -0,0 +1,33 @@
+Results of custom sorters
+
+
+
+sort by function task.file.filename
+Like 'sort by filename' but includes the file extension.
+=>
+- [ ] xyz in
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a_b_c.md
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/d/c.md
+- [ ] xyz in e/d/c.md
+====================================================================================
+
+
+sort by function task.file.filenameWithoutExtension
+Like 'sort by filename'.
+=>
+- [ ] xyz in
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a_b_c.md
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/d/c.md
+- [ ] xyz in e/d/c.md
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.folder_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.folder_docs.approved.md
new file mode 100644
index 00000000..a01d7239
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.folder_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.file.folder
+```
+
+- Enable sorting by the folder containing the task.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.folder_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.folder_results.approved.txt
new file mode 100644
index 00000000..02ce1977
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.folder_results.approved.txt
@@ -0,0 +1,18 @@
+Results of custom sorters
+
+
+
+sort by function task.file.folder
+Enable sorting by the folder containing the task.
+=>
+- [ ] xyz in
+- [ ] xyz in a_b_c.md
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/d/c.md
+- [ ] xyz in e/d/c.md
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.path_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.path_docs.approved.md
new file mode 100644
index 00000000..dafef5fd
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.path_docs.approved.md
@@ -0,0 +1,17 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.file.path
+```
+
+- Like 'Sort by path' but includes the file extension.
+
+```javascript
+sort by function task.file.pathWithoutExtension
+```
+
+- Like 'Sort by path'.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.path_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.path_results.approved.txt
new file mode 100644
index 00000000..6a13a3c8
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.path_results.approved.txt
@@ -0,0 +1,33 @@
+Results of custom sorters
+
+
+
+sort by function task.file.path
+Like 'Sort by path' but includes the file extension.
+=>
+- [ ] xyz in
+- [ ] xyz in a_b_c.md
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/d/c.md
+- [ ] xyz in e/d/c.md
+====================================================================================
+
+
+sort by function task.file.pathWithoutExtension
+Like 'Sort by path'.
+=>
+- [ ] xyz in
+- [ ] xyz in a_b_c.md
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/d/c.md
+- [ ] xyz in e/d/c.md
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.root_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.root_docs.approved.md
new file mode 100644
index 00000000..124d4f12
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.root_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.file.root
+```
+
+- Enable sorting by the root folder.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.root_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.root_results.approved.txt
new file mode 100644
index 00000000..312d536b
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.file.root_results.approved.txt
@@ -0,0 +1,18 @@
+Results of custom sorters
+
+
+
+sort by function task.file.root
+Enable sorting by the root folder.
+=>
+- [ ] xyz in
+- [ ] xyz in a_b_c.md
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in a/d/c.md
+- [ ] xyz in e/d/c.md
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.heading_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.heading_docs.approved.md
new file mode 100644
index 00000000..155ec5a3
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.heading_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.heading
+```
+
+- Like 'sort by heading'.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.heading_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.heading_results.approved.txt
new file mode 100644
index 00000000..9d9e28f5
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.file_properties_task.heading_results.approved.txt
@@ -0,0 +1,18 @@
+Results of custom sorters
+
+
+
+sort by function task.heading
+Like 'sort by heading'.
+=>
+- [ ] xyz in a_b_c.md
+- [ ] xyz in a/b/c.md
+- [ ] xyz in
+- [ ] xyz in a/d/c.md
+- [ ] xyz in e/d/c.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b.md
+- [ ] xyz in a/b/_c_.md
+- [ ] xyz in a/b/c.md
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.blockLink_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.blockLink_docs.approved.md
new file mode 100644
index 00000000..e72d1a9e
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.blockLink_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.blockLink
+```
+
+- DO NOT RELEASE UNTIL THE LEADING SPACE IS REMOVED FROM BLOCKLINKS. Removing the leading space and carat prevents the rendered heading itself being treated as a blocklink.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.blockLink_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.blockLink_results.approved.txt
new file mode 100644
index 00000000..9ec996e4
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.blockLink_results.approved.txt
@@ -0,0 +1,12 @@
+Results of custom sorters
+
+
+
+sort by function task.blockLink
+DO NOT RELEASE UNTIL THE LEADING SPACE IS REMOVED FROM BLOCKLINKS. Removing the leading space and carat prevents the rendered heading itself being treated as a blocklink.
+=>
+- [ ] my description
+- [ ] my description ^ca47c7
+- [ ] my description ^fromseparatefile
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.descriptionWithoutTags_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.descriptionWithoutTags_docs.approved.md
new file mode 100644
index 00000000..987e9f60
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.descriptionWithoutTags_docs.approved.md
@@ -0,0 +1,12 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.descriptionWithoutTags
+```
+
+- Like `Sort by description`, but it removes any tags from the sort key.
+- This might be useful for sorting together completed recurrences of the same task, even if the tags differ in some recurrences.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.descriptionWithoutTags_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.descriptionWithoutTags_results.approved.txt
new file mode 100644
index 00000000..3ba58d5b
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.descriptionWithoutTags_results.approved.txt
@@ -0,0 +1,20 @@
+Results of custom sorters
+
+
+
+sort by function task.descriptionWithoutTags
+Like `Sort by description`, but it removes any tags from the sort key.
+This might be useful for sorting together completed recurrences of the same task, even if the tags differ in some recurrences.
+=>
+- [ ] long description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce quam ipsum, consectetur ut dolor nec, fringilla lobortis mi. Vestibulum gravida tincidunt urna nec ornare. Cras sit amet sagittis sapien, vitae mattis velit. Vestibulum sem tortor, blandit at ultrices eget, ultrices eget odio. Donec efficitur purus massa, vel molestie turpis tincidunt id. 
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+- [ ] short description
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.description_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.description_docs.approved.md
new file mode 100644
index 00000000..b75870a0
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.description_docs.approved.md
@@ -0,0 +1,19 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.description.length
+```
+
+- Sort by length of description, shortest first.
+- This might be useful for finding tasks that need more information, or could be made less verbose.
+
+```javascript
+sort by function task.description.replace('🟥', 1).replace('🟧', 2).replace('🟨', 3).replace('🟩', 4).replace('🟦', 5)
+```
+
+- A user has defined custom system for their task descriptions, with coloured squares at the **start** of task lines as a home-grown alternative priority system.
+- This allows tasks to be sorted in the order of their coloured squares.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.description_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.description_results.approved.txt
new file mode 100644
index 00000000..ccf77cbb
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.description_results.approved.txt
@@ -0,0 +1,47 @@
+Results of custom sorters
+
+
+
+sort by function task.description.length
+Sort by length of description, shortest first.
+This might be useful for finding tasks that need more information, or could be made less verbose.
+=>
+- [ ] my description
+- [ ] short description
+- [ ] my description #tag
+- [ ] 🟦 pseudo low priority
+- [ ] 🟧 pseudo high priority
+- [ ] 🟩 pseudo normal priority
+- [ ] 🟨 pseudo medium priority
+- [ ] my description #tag/subtag
+- [ ] 🟥 pseudo highest priority
+- [ ] my description #context/home
+- [ ] my description #context/work
+- [ ] my description #project/project1
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] long description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce quam ipsum, consectetur ut dolor nec, fringilla lobortis mi. Vestibulum gravida tincidunt urna nec ornare. Cras sit amet sagittis sapien, vitae mattis velit. Vestibulum sem tortor, blandit at ultrices eget, ultrices eget odio. Donec efficitur purus massa, vel molestie turpis tincidunt id. 
+====================================================================================
+
+
+sort by function task.description.replace('🟥', 1).replace('🟧', 2).replace('🟨', 3).replace('🟩', 4).replace('🟦', 5)
+A user has defined custom system for their task descriptions, with coloured squares at the **start** of task lines as a home-grown alternative priority system.
+This allows tasks to be sorted in the order of their coloured squares.
+=>
+- [ ] 🟥 pseudo highest priority
+- [ ] 🟧 pseudo high priority
+- [ ] 🟨 pseudo medium priority
+- [ ] 🟩 pseudo normal priority
+- [ ] 🟦 pseudo low priority
+- [ ] long description Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce quam ipsum, consectetur ut dolor nec, fringilla lobortis mi. Vestibulum gravida tincidunt urna nec ornare. Cras sit amet sagittis sapien, vitae mattis velit. Vestibulum sem tortor, blandit at ultrices eget, ultrices eget odio. Donec efficitur purus massa, vel molestie turpis tincidunt id. 
+- [ ] my description
+- [ ] my description #context/home
+- [ ] my description #context/work
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] short description
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isDone_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isDone_docs.approved.md
new file mode 100644
index 00000000..df0e697b
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isDone_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.isDone
+```
+
+- Tasks with [[Status Types|Status Type]] `TODO` and `IN_PROGRESS` tasks are sorted before those with types `DONE`, `CANCELLED` and `NON_TASK.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isDone_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isDone_results.approved.txt
new file mode 100644
index 00000000..73c9360d
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isDone_results.approved.txt
@@ -0,0 +1,15 @@
+Results of custom sorters
+
+
+
+sort by function task.isDone
+Tasks with [[Status Types|Status Type]] `TODO` and `IN_PROGRESS` tasks are sorted before those with types `DONE`, `CANCELLED` and `NON_TASK.
+=>
+- [ ] Status Todo
+- [] Status EMPTY
+- [/] Status In Progress
+- [-] Status Cancelled
+- [x] Status Done
+- [Q] Status Non-Task
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isRecurring_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isRecurring_docs.approved.md
new file mode 100644
index 00000000..2845995b
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isRecurring_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.isRecurring
+```
+
+- Sort by whether the task is recurring.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isRecurring_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isRecurring_results.approved.txt
new file mode 100644
index 00000000..8553efd3
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.isRecurring_results.approved.txt
@@ -0,0 +1,23 @@
+Results of custom sorters
+
+
+
+sort by function task.isRecurring
+Sort by whether the task is recurring.
+=>
+- [ ] my description
+- [ ] my description 🔁 every 4 months on the 3rd Wednesday
+- [ ] my description 🔁 every month
+- [ ] my description 🔁 every month on the 2nd
+- [ ] my description 🔁 every month on the 2nd when done
+- [ ] my description 🔁 every week on Tuesday
+- [ ] my description 🔁 every week on Tuesday when done
+- [ ] my description 🔁 every week
+- [ ] my description 🔁 every 3 weeks on Thursday
+- [ ] my description 🔁 every 4 weeks
+- [ ] my description 🔁 every 6 days
+- [ ] my description 🔁 every 8 days
+- [ ] my description 🔁 every 8 days when done
+- [ ] my description 🔁 every day
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.originalMarkdown_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.originalMarkdown_docs.approved.md
new file mode 100644
index 00000000..413f0440
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.originalMarkdown_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.originalMarkdown
+```
+
+- Sort by the raw text of the task's original line in the MarkDown file.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.originalMarkdown_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.originalMarkdown_results.approved.txt
new file mode 100644
index 00000000..81fe7cab
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.originalMarkdown_results.approved.txt
@@ -0,0 +1,17 @@
+Results of custom sorters
+
+
+
+sort by function task.originalMarkdown
+Sort by the raw text of the task's original line in the MarkDown file.
+=>
+- [ ] my description
+- [ ] my description #context/home
+- [ ] my description #context/work
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityName_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityName_docs.approved.md
new file mode 100644
index 00000000..bdcecfea
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityName_docs.approved.md
@@ -0,0 +1,13 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.priorityName
+```
+
+- Sort by the task's priority name.
+- The priority names are displayed in alphabetical order.
+- Note that the default priority is called 'Normal', as opposed to with `Sort by priority` which calls the default 'None'.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityName_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityName_results.approved.txt
new file mode 100644
index 00000000..9d248f6d
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityName_results.approved.txt
@@ -0,0 +1,17 @@
+Results of custom sorters
+
+
+
+sort by function task.priorityName
+Sort by the task's priority name.
+The priority names are displayed in alphabetical order.
+Note that the default priority is called 'Normal', as opposed to with `Sort by priority` which calls the default 'None'.
+=>
+- [ ] #task High priority ⏫
+- [ ] #task Highest priority 🔺
+- [ ] #task Low priority 🔽
+- [ ] #task Lowest priority ⏬
+- [ ] #task Medium priority 🔼
+- [ ] #task Normal priority
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityNumber_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityNumber_docs.approved.md
new file mode 100644
index 00000000..e9f197b5
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityNumber_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.priorityNumber
+```
+
+- Sort by the task's priority number, where Highest is 0 and Lowest is 5.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityNumber_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityNumber_results.approved.txt
new file mode 100644
index 00000000..f4733e1e
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.priorityNumber_results.approved.txt
@@ -0,0 +1,15 @@
+Results of custom sorters
+
+
+
+sort by function task.priorityNumber
+Sort by the task's priority number, where Highest is 0 and Lowest is 5.
+=>
+- [ ] #task Highest priority 🔺
+- [ ] #task High priority ⏫
+- [ ] #task Medium priority 🔼
+- [ ] #task Normal priority
+- [ ] #task Low priority 🔽
+- [ ] #task Lowest priority ⏬
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.recurrenceRule_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.recurrenceRule_docs.approved.md
new file mode 100644
index 00000000..d256f0aa
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.recurrenceRule_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.recurrenceRule
+```
+
+- Sort by recurrence rule.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.recurrenceRule_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.recurrenceRule_results.approved.txt
new file mode 100644
index 00000000..2843a866
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.recurrenceRule_results.approved.txt
@@ -0,0 +1,23 @@
+Results of custom sorters
+
+
+
+sort by function task.recurrenceRule
+Sort by recurrence rule.
+=>
+- [ ] my description
+- [ ] my description 🔁 every 3 weeks on Thursday
+- [ ] my description 🔁 every 4 months on the 3rd Wednesday
+- [ ] my description 🔁 every 4 weeks
+- [ ] my description 🔁 every 6 days
+- [ ] my description 🔁 every 8 days
+- [ ] my description 🔁 every 8 days when done
+- [ ] my description 🔁 every day
+- [ ] my description 🔁 every month
+- [ ] my description 🔁 every month on the 2nd
+- [ ] my description 🔁 every month on the 2nd when done
+- [ ] my description 🔁 every week
+- [ ] my description 🔁 every week on Tuesday
+- [ ] my description 🔁 every week on Tuesday when done
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags.advanced_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags.advanced_docs.approved.md
new file mode 100644
index 00000000..ee400937
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags.advanced_docs.approved.md
@@ -0,0 +1,5 @@
+<!-- placeholder to force blank line before included text -->
+
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags.advanced_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags.advanced_results.approved.txt
new file mode 100644
index 00000000..977e572a
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags.advanced_results.approved.txt
@@ -0,0 +1,115 @@
+Results of custom sorters
+
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[0].replace('#', '') )
+`#tag/subtag/sub-sub-tag` gives **`tag`**.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[1] ? tag.split('/').slice(1, 2) : '')
+`#tag/subtag/sub-sub-tag` gives **`subtag`**.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[2] ? tag.split('/').slice(2, 3) : '')
+`#tag/subtag/sub-sub-tag` gives **`sub-sub-tag`**.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[3] ? tag.split('/').slice(3, 4) : '')
+`#tag/subtag/sub-sub-tag` gives no heading, as there is no value at the 4th level.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[0] )
+`#tag/subtag/sub-sub-tag` gives **`#tag`**.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[1] ? tag.split('/').slice(0, 2).join('/') : '')
+`#tag/subtag/sub-sub-tag` gives **`#tag/subtag`**.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[2] ? tag.split('/').slice(0, 3).join('/') : '')
+`#tag/subtag/sub-sub-tag` gives **`#tag/subtag/sub-sub-tag`**.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function task.tags.map( (tag) => tag.split('/')[3] ? tag.split('/').slice(0, 4).join('/') : '')
+`#tag/subtag/sub-sub-tag` gives no heading, as there is no value at the 4th level.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags_docs.approved.md
new file mode 100644
index 00000000..abd915ec
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags_docs.approved.md
@@ -0,0 +1,25 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.tags.filter( (tag) => tag.includes("#context/")).sort().join(",")
+```
+
+- Sort by tags that contain "#context/".
+- Any tasks without that tag are sorted first.
+
+```javascript
+sort by function reverse task.tags.length
+```
+
+- Sort by the number of tags on the task.
+- The `reverse` option puts tasks with the most tags first.
+
+```javascript
+sort by function -task.tags.length
+```
+
+- A different way of sorting by the number of tags on the task, still putting tasks with the most tags first.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags_results.approved.txt
new file mode 100644
index 00000000..0c9affa5
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.tags_results.approved.txt
@@ -0,0 +1,47 @@
+Results of custom sorters
+
+
+
+sort by function task.tags.filter( (tag) => tag.includes("#context/")).sort().join(",")
+Sort by tags that contain "#context/".
+Any tasks without that tag are sorted first.
+=>
+- [ ] my description
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+====================================================================================
+
+
+sort by function reverse task.tags.length
+Sort by the number of tags on the task.
+The `reverse` option puts tasks with the most tags first.
+=>
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+- [ ] my description
+====================================================================================
+
+
+sort by function -task.tags.length
+A different way of sorting by the number of tags on the task, still putting tasks with the most tags first.
+=>
+- [ ] my description #multiple-tags1 #multiple-tags2
+- [ ] my description #tag
+- [ ] my description #tag/subtag
+- [ ] my description #tag/subtag/sub-sub-tag
+- [ ] my description #project/project1
+- [ ] my description #context/home
+- [ ] my description #context/work
+- [ ] my description
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.urgency_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.urgency_docs.approved.md
new file mode 100644
index 00000000..68c2bbd2
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.urgency_docs.approved.md
@@ -0,0 +1,12 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function reverse task.urgency
+```
+
+- Sort by task urgency values.
+- We use `reverse` to put the most urgent tasks first.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.urgency_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.urgency_results.approved.txt
new file mode 100644
index 00000000..d2e4a158
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.other_properties_task.urgency_results.approved.txt
@@ -0,0 +1,17 @@
+Results of custom sorters
+
+
+
+sort by function reverse task.urgency
+Sort by task urgency values.
+We use `reverse` to put the most urgent tasks first.
+=>
+- [ ] due 2023-06-11 📅 2023-06-11
+- [ ] #task Highest priority 🔺
+- [ ] #task High priority ⏫
+- [ ] #task Medium priority 🔼
+- [ ] #task Normal priority
+- [ ] #task Low priority 🔽
+- [ ] #task Lowest priority ⏬
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.name_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.name_docs.approved.md
new file mode 100644
index 00000000..739a0221
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.name_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.status.name
+```
+
+- Identical to "Sort by status.name".
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.name_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.name_results.approved.txt
new file mode 100644
index 00000000..ef336d2c
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.name_results.approved.txt
@@ -0,0 +1,15 @@
+Results of custom sorters
+
+
+
+sort by function task.status.name
+Identical to "Sort by status.name".
+=>
+- [-] Status Cancelled
+- [x] Status Done
+- [] Status EMPTY
+- [/] Status In Progress
+- [Q] Status Non-Task
+- [ ] Status Todo
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.nextSymbol_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.nextSymbol_docs.approved.md
new file mode 100644
index 00000000..218abe07
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.nextSymbol_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.status.nextSymbol
+```
+
+- Sort by the next status symbol.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.nextSymbol_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.nextSymbol_results.approved.txt
new file mode 100644
index 00000000..a017cdd8
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.nextSymbol_results.approved.txt
@@ -0,0 +1,15 @@
+Results of custom sorters
+
+
+
+sort by function task.status.nextSymbol
+Sort by the next status symbol.
+=>
+- [] Status EMPTY
+- [-] Status Cancelled
+- [x] Status Done
+- [Q] Status Non-Task
+- [ ] Status Todo
+- [/] Status In Progress
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.symbol_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.symbol_docs.approved.md
new file mode 100644
index 00000000..5f5124e3
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.symbol_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.status.symbol
+```
+
+- Sort by the status symbol.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.symbol_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.symbol_results.approved.txt
new file mode 100644
index 00000000..dbe2335f
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.symbol_results.approved.txt
@@ -0,0 +1,15 @@
+Results of custom sorters
+
+
+
+sort by function task.status.symbol
+Sort by the status symbol.
+=>
+- [] Status EMPTY
+- [ ] Status Todo
+- [-] Status Cancelled
+- [/] Status In Progress
+- [Q] Status Non-Task
+- [x] Status Done
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.type_docs.approved.md b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.type_docs.approved.md
new file mode 100644
index 00000000..03ca262c
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.type_docs.approved.md
@@ -0,0 +1,11 @@
+<!-- placeholder to force blank line before included text -->
+
+
+```javascript
+sort by function task.status.type
+```
+
+- Unlike "Sort by status.type", this sorts the status types in alphabetical order.
+
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.type_results.approved.txt b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.type_results.approved.txt
new file mode 100644
index 00000000..ab770521
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.statuses_task.status.type_results.approved.txt
@@ -0,0 +1,15 @@
+Results of custom sorters
+
+
+
+sort by function task.status.type
+Unlike "Sort by status.type", this sorts the status types in alphabetical order.
+=>
+- [-] Status Cancelled
+- [x] Status Done
+- [] Status EMPTY
+- [/] Status In Progress
+- [Q] Status Non-Task
+- [ ] Status Todo
+====================================================================================
+
diff --git a/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.ts b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.ts
new file mode 100644
index 00000000..d81c6ca0
--- /dev/null
+++ b/tests/Scripting/ScriptingReference/CustomSorting/CustomSortingExamples.test.ts
@@ -0,0 +1,373 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import moment from 'moment';
+import type { Task } from '../../../../src/Task';
+import { SampleTasks, fromLine, fromLines } from '../../../TestHelpers';
+import type { CustomPropertyDocsTestData, QueryInstructionLineAndDescription } from '../VerifyFunctionFieldSamples';
+import {
+    verifyFunctionFieldFilterSamplesForDocs,
+    verifyFunctionFieldSortSamplesOnTasks,
+} from '../VerifyFunctionFieldSamples';
+
+window.moment = moment;
+
+beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(new Date('2023-06-10 20:00'));
+});
+
+afterEach(() => {
+    jest.useRealTimers();
+});
+
+// NEW_QUERY_INSTRUCTION_EDIT_REQUIRED
+
+describe('dates', () => {
+    const testData: CustomPropertyDocsTestData[] = [
+        // ---------------------------------------------------------------------------------
+        // DATE FIELDS
+        // ---------------------------------------------------------------------------------
+
+        [
+            'task.cancelled',
+            [
+                [
+                    'sort by function task.cancelled.format("dddd")',
+                    "Sort by cancelled date's day of the week, alphabetically",
+                ],
+            ],
+            SampleTasks.withAllRepresentativeCancelledDates(),
+        ],
+
+        [
+            'task.created',
+            [
+                [
+                    'sort by function task.created.format("dddd")',
+                    "Sort by created date's day of the week, alphabetically",
+                ],
+            ],
+            SampleTasks.withAllRepresentativeCreatedDates(),
+        ],
+
+        [
+            'task.done',
+            [['sort by function task.done.format("dddd")', "Sort by done date's day of the week, alphabetically"]],
+            SampleTasks.withAllRepresentativeDoneDates(),
+        ],
+
+        [
+            'task.due',
+            [['sort by function task.due.format("dddd")', "Sort by due date's day of the week, alphabetically"]],
+            SampleTasks.withAllRepresentativeDueDates(),
+        ],
+
+        ['task.due.advanced', [], SampleTasks.withAllRepresentativeDueDates()],
+
+        [
+            'task.happens',
+            [
+                [
+                    'sort by function task.happens.format("dddd")',
+                    "Sort by happens date's day of the week, alphabetically",
+                ],
+            ],
+            SampleTasks.withAllRepresentativeDueDates(),
+        ],
+
+        [
+            'task.scheduled',
+            [
+                [
+                    'sort by function task.scheduled.format("dddd")',
+                    "Sort by scheduled date's day of the week, alphabetically",
+                ],
+            ],
+            SampleTasks.withAllRepresentativeScheduledDates(),
+        ],
+
+        [
+            'task.start',
+            [['sort by function task.start.format("dddd")', "Sort by start date's day of the week, alphabetically"]],
+            SampleTasks.withAllRepresentativeStartDates(),
+        ],
+    ];
+
+    it.each(testData)('%s results', (_: string, groups: QueryInstructionLineAndDescription[], tasks: Task[]) => {
+        verifyFunctionFieldSortSamplesOnTasks(groups, tasks);
+    });
+
+    it.each(testData)('%s docs', (_: string, groups: QueryInstructionLineAndDescription[], _tasks: Task[]) => {
+        verifyFunctionFieldFilterSamplesForDocs(groups);
+    });
+});
+
+describe('file properties', () => {
+    const tasks = SampleTasks.withAllRootsPathsHeadings();
+
+    const testData: CustomPropertyDocsTestData[] = [
+        // ---------------------------------------------------------------------------------
+        // FILE FIELDS
+        // ---------------------------------------------------------------------------------
+
+        [
+            'task.file.path',
+            [
+                // comment to force line break
+                ['sort by function task.file.path', "Like 'Sort by path' but includes the file extension"],
+                ['sort by function task.file.pathWithoutExtension', "Like 'Sort by path'"],
+            ],
+            tasks,
+        ],
+
+        [
+            'task.file.root',
+            [
+                // comment to force line break
+                ['sort by function task.file.root', 'Enable sorting by the root folder'],
+            ],
+            tasks,
+        ],
+
+        [
+            'task.file.folder',
+            [
+                // comment to force line break
+                ['sort by function task.file.folder', 'Enable sorting by the folder containing the task'],
+            ],
+            tasks,
+        ],
+
+        [
+            'task.file.filename',
+            [
+                ['sort by function task.file.filename', "Like 'sort by filename' but includes the file extension"],
+                ['sort by function task.file.filenameWithoutExtension', "Like 'sort by filename'"],
+            ],
+            tasks,
+        ],
+
+        ['task.heading', [['sort by function task.heading', "Like 'sort by heading'"]], tasks],
+    ];
+
+    it.each(testData)('%s results', (_: string, groups: QueryInstructionLineAndDescription[], tasks: Task[]) => {
+        verifyFunctionFieldSortSamplesOnTasks(groups, tasks);
+    });
+
+    it.each(testData)('%s docs', (_: string, groups: QueryInstructionLineAndDescription[], _tasks: Task[]) => {
+        verifyFunctionFieldFilterSamplesForDocs(groups);
+    });
+});
+
+describe('statuses', () => {
+    const tasks = SampleTasks.withAllStatuses();
+
+    const testData: CustomPropertyDocsTestData[] = [
+        ['task.status.name', [['sort by function task.status.name', 'Identical to "Sort by status.name"']], tasks],
+
+        [
+            'task.status.nextSymbol',
+            [['sort by function task.status.nextSymbol', 'Sort by the next status symbol']],
+            tasks,
+        ],
+
+        ['task.status.symbol', [['sort by function task.status.symbol', 'Sort by the status symbol']], tasks],
+
+        [
+            'task.status.type',
+            [
+                [
+                    'sort by function task.status.type',
+                    'Unlike "Sort by status.type", this sorts the status types in alphabetical order',
+                ],
+            ],
+            tasks,
+        ],
+    ];
+
+    it.each(testData)('%s results', (_: string, groups: QueryInstructionLineAndDescription[], tasks: Task[]) => {
+        verifyFunctionFieldSortSamplesOnTasks(groups, tasks);
+    });
+
+    it.each(testData)('%s docs', (_: string, groups: QueryInstructionLineAndDescription[], _tasks: Task[]) => {
+        verifyFunctionFieldFilterSamplesForDocs(groups);
+    });
+});
+
+describe('other properties', () => {
+    const pseudoCustomPriorityEmojis = fromLines({
+        lines: [
+            '- [ ] 🟦 pseudo low priority',
+            '- [ ] 🟧 pseudo high priority',
+            '- [ ] 🟩 pseudo normal priority',
+            '- [ ] 🟥 pseudo highest priority',
+            '- [ ] 🟨 pseudo medium priority',
+        ],
+    });
+
+    const testData: CustomPropertyDocsTestData[] = [
+        // ---------------------------------------------------------------------------------
+        // RECURRENCE FIELDS
+        // ---------------------------------------------------------------------------------
+
+        [
+            'task.isRecurring',
+            [['sort by function task.isRecurring', 'Sort by whether the task is recurring']],
+            SampleTasks.withAllRecurrences(),
+        ],
+
+        [
+            'task.recurrenceRule',
+            [['sort by function task.recurrenceRule', 'Sort by recurrence rule']],
+            SampleTasks.withAllRecurrences(),
+        ],
+
+        // ---------------------------------------------------------------------------------
+        // OTHER FIELDS
+        // ---------------------------------------------------------------------------------
+        [
+            'task.blockLink',
+            [
+                [
+                    'sort by function task.blockLink',
+                    'DO NOT RELEASE UNTIL THE LEADING SPACE IS REMOVED FROM BLOCKLINKS. Removing the leading space and carat prevents the rendered heading itself being treated as a blocklink.',
+                ],
+            ],
+            SampleTasks.withAllRepresentativeBlockLinks(),
+        ],
+
+        [
+            'task.description',
+            [
+                [
+                    'sort by function task.description.length',
+                    'Sort by length of description, shortest first.',
+                    'This might be useful for finding tasks that need more information, or could be made less verbose',
+                ],
+                [
+                    "sort by function task.description.replace('🟥', 1).replace('🟧', 2).replace('🟨', 3).replace('🟩', 4).replace('🟦', 5)",
+                    'A user has defined custom system for their task descriptions, with coloured squares at the **start** of task lines as a home-grown alternative priority system.',
+                    'This allows tasks to be sorted in the order of their coloured squares.',
+                ],
+            ],
+            SampleTasks.withAllRepresentativeDescriptions().concat(
+                SampleTasks.withRepresentativeTags().concat(pseudoCustomPriorityEmojis),
+            ),
+        ],
+
+        [
+            'task.descriptionWithoutTags',
+            [
+                [
+                    'sort by function task.descriptionWithoutTags',
+                    'Like `Sort by description`, but it removes any tags from the sort key.',
+                    'This might be useful for sorting together completed recurrences of the same task, even if the tags differ in some recurrences',
+                ],
+            ],
+            SampleTasks.withAllRepresentativeDescriptions().concat(SampleTasks.withRepresentativeTags()),
+        ],
+
+        // [
+        //     'task.indentation',
+        //     [['sort by function task.indentation', '...']],
+        //     SampleTasks.withAllPriorities(), // TODO Choose specific tasks for task.indentation'
+        // ],
+
+        [
+            'task.isDone',
+            [
+                [
+                    'sort by function task.isDone',
+                    'Tasks with [[Status Types|Status Type]] `TODO` and `IN_PROGRESS` tasks are sorted before those with types `DONE`, `CANCELLED` and `NON_TASK.',
+                ],
+            ],
+            SampleTasks.withAllStatuses(),
+        ],
+
+        // [
+        //     'task.listMarker',
+        //     [['sort by function task.listMarker', '...']],
+        //     SampleTasks.withAllPriorities(), // TODO Choose specific tasks for task.listMarker'
+        // ],
+
+        [
+            'task.priorityName',
+            [
+                [
+                    'sort by function task.priorityName',
+                    "Sort by the task's priority name",
+                    'The priority names are displayed in alphabetical order.',
+                    "Note that the default priority is called 'Normal', as opposed to with `Sort by priority` which calls the default 'None'",
+                ],
+            ],
+            SampleTasks.withAllPriorities(),
+        ],
+
+        [
+            'task.priorityNumber',
+            [
+                [
+                    'sort by function task.priorityNumber',
+                    "Sort by the task's priority number, where Highest is 0 and Lowest is 5",
+                ],
+            ],
+            SampleTasks.withAllPriorities(),
+        ],
+
+        [
+            'task.tags',
+            [
+                [
+                    // TODO Make this simpler, by allowing sort-keys that are arrays - and joining them by ','
+                    'sort by function task.tags.filter( (tag) => tag.includes("#context/")).sort().join(",")',
+                    'Sort by tags that contain "#context/".',
+                    'Any tasks without that tag are sorted first.',
+                ],
+                [
+                    'sort by function reverse task.tags.length',
+                    'Sort by the number of tags on the task',
+                    'The `reverse` option puts tasks with the most tags first.',
+                ],
+                [
+                    'sort by function -task.tags.length',
+                    'A different way of sorting by the number of tags on the task, still putting tasks with the most tags first.',
+                ],
+            ],
+            SampleTasks.withRepresentativeTags(),
+        ],
+        ['task.tags.advanced', [], SampleTasks.withRepresentativeTags()],
+
+        [
+            'task.originalMarkdown',
+            [
+                [
+                    'sort by function task.originalMarkdown',
+                    "Sort by the raw text of the task's original line in the MarkDown file.",
+                ],
+            ],
+            SampleTasks.withRepresentativeTags(),
+        ],
+
+        [
+            'task.urgency',
+            [
+                [
+                    'sort by function reverse task.urgency',
+                    'Sort by task urgency values.',
+                    'We use `reverse` to put the most urgent tasks first.',
+                ],
+            ],
+            SampleTasks.withAllPriorities().concat(fromLine({ line: '- [ ] due 2023-06-11 📅 2023-06-11' })),
+        ],
+    ];
+
+    it.each(testData)('%s results', (_: string, groups: QueryInstructionLineAndDescription[], tasks: Task[]) => {
+        verifyFunctionFieldSortSamplesOnTasks(groups, tasks);
+    });
+
+    it.each(testData)('%s docs', (_: string, groups: QueryInstructionLineAndDescription[], _tasks: Task[]) => {
+        verifyFunctionFieldFilterSamplesForDocs(groups);
+    });
+});
diff --git a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
index 426dce39..45ce65c0 100644
--- a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
+++ b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
@@ -7,6 +7,8 @@ import { expandPlaceholders } from '../../../src/Scripting/ExpandPlaceholders';
 import { makeQueryContext } from '../../../src/Scripting/QueryContext';
 import { scan } from '../../../src/Query/Scanner';
 import { SearchInfo } from '../../../src/Query/SearchInfo';
+import { Sort } from '../../../src/Query/Sort';
+import { toLines } from '../../TestHelpers';
 
 /** For example, 'task.due' */
 type TaskPropertyName = string;
@@ -108,6 +110,31 @@ export function verifyFunctionFieldFilterSamplesForDocs(filters: QueryInstructio
     verifyMarkdownForDocs(markdown);
 }
 
+// -----------------------------------------------------------------------------------------------------------------
+// Sorting
+// -----------------------------------------------------------------------------------------------------------------
+
+export function verifyFunctionFieldSortSamplesOnTasks(
+    instructions: QueryInstructionLineAndDescription[],
+    tasks: Task[],
+) {
+    if (instructions.length === 0) {
+        return;
+    }
+    verifyAll('Results of custom sorters', instructions, (filter) => {
+        const instruction = filter[0];
+        const comment = filter.slice(1);
+
+        const path = 'a/b.md';
+        const expandedInstruction = preprocessSingleInstruction(instruction, path);
+        const sorter = new FunctionField().createSorterFromLine(expandedInstruction);
+        expect(sorter).not.toBeNull();
+
+        const tasksSorted = Sort.by([sorter!], tasks);
+        return formatQueryAndResultsForApproving(instruction, comment, toLines(tasksSorted));
+    });
+}
+
 // -----------------------------------------------------------------------------------------------------------------
 // Grouping
 // -----------------------------------------------------------------------------------------------------------------
diff --git a/tests/TestHelpers.ts b/tests/TestHelpers.ts
index f6358747..711caa64 100644
--- a/tests/TestHelpers.ts
+++ b/tests/TestHelpers.ts
@@ -34,6 +34,14 @@ export function fromLines({
     return lines.map((line) => fromLine({ line, path, precedingHeader }));
 }
 
+export function toLine(task: Task) {
+    return task.toFileLineString();
+}
+
+export function toLines(tasks: Task[]) {
+    return tasks.map((task) => toLine(task));
+}
+
 export function createTasksFromMarkdown(tasksAsMarkdown: string, path: string, precedingHeader: string): Task[] {
     const taskLines = tasksAsMarkdown.split('\n');
     const tasks: Task[] = [];
@@ -202,6 +210,7 @@ export class SampleTasks {
             Status.makeEmpty(),
             Status.makeInProgress(),
             Status.makeTodo(),
+            Status.makeNonTask(),
         ];
 
         return statuses.map((status) => {