Repository: obsidian-tasks-group/obsidian-tasks (ts)

Issue:
Fix the handling of special characters in Mermaid status diagrams
### Please check that this issue hasn't been reported before.

- [X] I searched previous [Bug Reports](https://github.com/obsidian-tasks-group/obsidian-tasks/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22+sort%3Acomments-desc) didn't find any similar reports.

### Expected Behavior

When generating Mermaid diagrams from user statuses (see #2354), Tasks should represent the following status symbols correctly, without error:

```text
- ["] Task 1
- [<] Task 2
- [>] Task 3
- [&] Task 4
```



### Current behaviour

An error message is displayed - see `Steps to reproduce` below.

### Steps to reproduce

1. Copy the patch in the first comment in https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2354
2. Build and run the Tasks-Demo vault
3. Create a note and paste in this text:

```text
- ["] Task 1
- [<] Task 2
- [>] Task 3
- [&] Task 4
```

4. Go to Tasks settings, Click "Add all Unknown Status Types"
5. Scroll down to "Create file with Mermaid diagram of Statuses"
    - **Note: This button is only present step 1 was done**
6. View the newly created file in Reading mode.

**Expected behaviour**

A mermaid diagram showing the statuses

**Actual behaviour**

Error text:

```text
Error parsing Mermaid diagram!

Parse error on line 12:
...O)"]11["'Unknown (")'<br>["] -> [x]<br>
----------------------^
Expecting 'SPACE', 'GRAPH', 'DIR', 'subgraph',
'SQE', 'end', 'AMP', 'ALPHA', 'COLON', 'TAGEND',
'START_LINK', 'STYLE', 'LINKSTYLE', 'CLASSDEF',
'CLASS', 'CLICK', 'DOWN', 'UP', 'DEFAULT', 'NUM',
'COMMA', 'MINUS', 'BRKT', 'DOT', 'PCT',
'TAGSTART', 'PUNCTUATION', 'UNICODE_TEXT',
'PLUS', 'EQUALS', 'MULT', 'UNDERSCORE', got 'PE'
```


### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

1.14.16

### Tasks Plugin Version

Unreleased version of tasks - with draft implementation of #2354

### Checks

- [X] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

Escape the special characters in Mermaid code - see [this StackOverflow answer](https://stackoverflow.com/a/71011447/104370).

Pull request:
fix: Correct (unreleased) display of special chars in Mermaid status diagrams
<!--- Provide a general summary of your changes in the Title above -->

# Description

See the description of the problem in #2355.

## Motivation and Context

Fixes #2355

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

## How has this been tested?

- Writing new tests
- Manually testing in the demo vault
<!--- 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. -->

## 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] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
    - But note that this code has not yet been released, so the bug was not visible to users.
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
    - Diagrams updated, but note that the changes in diagrams are not visible to 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`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] 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/Example Statuses.md b/docs/Getting Started/Statuses/Example Statuses.md
index 50d7841c..f530be0b 100644
--- a/docs/Getting Started/Statuses/Example Statuses.md
+++ b/docs/Getting Started/Statuses/Example Statuses.md
@@ -19,9 +19,9 @@ This might be useful if you have a few slightly more time-consuming tasks to kee
 <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.mermaid.md -->
 ```mermaid
 flowchart LR
-1[Todo]
-2[In Progress]
-3[Done]
+1["Todo"]
+2["In Progress"]
+3["Done"]
 1 --> 2
 2 --> 3
 3 --> 1
@@ -47,9 +47,9 @@ By using non-standard symbols for the `IN_PROGRESS` and `DONE` statuses, if I de
 <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.mermaid.md -->
 ```mermaid
 flowchart LR
-1[Important]
-2[Doing - Important]
-3[Done - Important]
+1["Important"]
+2["Doing - Important"]
+3["Done - Important"]
 1 --> 2
 2 --> 3
 3 --> 1
@@ -77,8 +77,8 @@ Either way, I can make them toggle to each other, and by giving them the type `N
 <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.mermaid.md -->
 ```mermaid
 flowchart LR
-1[Pro]
-2[Con]
+1["Pro"]
+2["Con"]
 1 --> 2
 2 --> 1
 ```
@@ -104,11 +104,11 @@ Here are some statuses from the ITS Theme where this behaviour might be useful.
 <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_toggle-does-nothing.approved.mermaid.md -->
 ```mermaid
 flowchart LR
-1[Bookmark]
-2[Example]
-3[Information]
-4[Paraphrase]
-5[Quote]
+1["Bookmark"]
+2["Example"]
+3["Information"]
+4["Paraphrase"]
+5["Quote"]
 1 --> 1
 2 --> 2
 3 --> 3
diff --git a/src/StatusRegistry.ts b/src/StatusRegistry.ts
index e0744d19..e805b160 100644
--- a/src/StatusRegistry.ts
+++ b/src/StatusRegistry.ts
@@ -1,5 +1,6 @@
 import { Status } from './Status';
 import { StatusConfiguration, StatusType } from './StatusConfiguration';
+import { htmlEncodeCharacter, htmlEncodeString } from './lib/HTMLCharacterEntities';
 
 /**
  * Tracks all the registered statuses a task can have.
@@ -305,14 +306,20 @@ export class StatusRegistry {
         const nodes: string[] = [];
         const edges: string[] = [];
         uniqueStatuses.forEach((status, index) => {
+            const statusName = htmlEncodeString(status.name);
             if (includeDetails) {
-                const transition = `[${status.symbol}] -> [${status.nextStatusSymbol}]`;
-                const statusName = `'${status.name}'`;
-                const statusType = `(${status.type})`;
-                const text = `${index + 1}["${statusName}<br>${transition}<br>${statusType}"]`;
+                const statusSymbol = htmlEncodeCharacter(status.symbol);
+                const statusNextStatusSymbol = htmlEncodeCharacter(status.nextStatusSymbol);
+                const statusType = status.type;
+
+                const transitionText = `[${statusSymbol}] -> [${statusNextStatusSymbol}]`;
+                const statusNameText = `'${statusName}'`;
+                const statusTypeText = `(${statusType})`;
+
+                const text = `${index + 1}["${statusNameText}<br>${transitionText}<br>${statusTypeText}"]`;
                 nodes.push(text);
             } else {
-                nodes.push(`${index + 1}[${status.name}]`);
+                nodes.push(`${index + 1}["${statusName}"]`);
             }
 
             // Check the next status:
diff --git a/src/lib/HTMLCharacterEntities.ts b/src/lib/HTMLCharacterEntities.ts
new file mode 100644
index 00000000..933e841b
--- /dev/null
+++ b/src/lib/HTMLCharacterEntities.ts
@@ -0,0 +1,37 @@
+/**
+ * Convert any single reserved HTML character to its entity name.
+ * @param character
+ *
+ * @see htmlEncodeString
+ */
+export function htmlEncodeCharacter(character: string) {
+    const charactersToEntityNames: { [index: string]: string } = {
+        '<': '&lt;',
+        '>': '&gt;',
+        '&': '&amp;',
+        '"': '&quot;',
+    };
+
+    const candidateEntityName = charactersToEntityNames[character];
+    if (candidateEntityName !== undefined) {
+        return candidateEntityName;
+    }
+
+    return character;
+}
+
+/**
+ * Convert reserved HTML characters to their entity names.
+ * @param characters
+ *
+ * @see htmlEncodeCharacter
+ */
+export function htmlEncodeString(characters: string) {
+    const chars = [...characters];
+    let result = '';
+    chars.forEach((c) => {
+        result += htmlEncodeCharacter(c);
+    });
+
+    return result;
+}


Test patch:
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.mermaid.md
index 8974e778..2ed7740a 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.mermaid.md
@@ -1,8 +1,8 @@
 
 ```mermaid
 flowchart LR
-1[Todo]
-2[Done]
+1["Todo"]
+2["Done"]
 1 --> 2
 2 --> 1
 ```
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_custom-statuses.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_custom-statuses.approved.mermaid.md
index d7f767c1..cac2f746 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_custom-statuses.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_custom-statuses.approved.mermaid.md
@@ -1,7 +1,7 @@
 
 ```mermaid
 flowchart LR
-1[In Progress]
-2[Cancelled]
+1["In Progress"]
+2["Cancelled"]
 
 ```
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.mermaid.md
index e74daf81..23600bf9 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.mermaid.md
@@ -1,9 +1,9 @@
 
 ```mermaid
 flowchart LR
-1[Important]
-2[Doing - Important]
-3[Done - Important]
+1["Important"]
+2["Doing - Important"]
+3["Done - Important"]
 1 --> 2
 2 --> 3
 3 --> 1
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.mermaid.md
index 43cf3f0a..5bc9d008 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_pro-con-cycle.approved.mermaid.md
@@ -1,8 +1,8 @@
 
 ```mermaid
 flowchart LR
-1[Pro]
-2[Con]
+1["Pro"]
+2["Con"]
 1 --> 2
 2 --> 1
 ```
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.mermaid.md
index 8bd6f89b..b21619ba 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_todo-in_progress-done.approved.mermaid.md
@@ -1,9 +1,9 @@
 
 ```mermaid
 flowchart LR
-1[Todo]
-2[In Progress]
-3[Done]
+1["Todo"]
+2["In Progress"]
+3["Done"]
 1 --> 2
 2 --> 3
 3 --> 1
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_toggle-does-nothing.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_toggle-does-nothing.approved.mermaid.md
index 4fc5a0fb..c2b2de3a 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_toggle-does-nothing.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.DefaultStatuses_toggle-does-nothing.approved.mermaid.md
@@ -1,11 +1,11 @@
 
 ```mermaid
 flowchart LR
-1[Bookmark]
-2[Example]
-3[Information]
-4[Paraphrase]
-5[Quote]
+1["Bookmark"]
+2["Example"]
+3["Information"]
+4["Paraphrase"]
+5["Quote"]
 1 --> 1
 2 --> 2
 3 --> 3
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_AnuPpuccin_Table.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_AnuPpuccin_Table.approved.mermaid.md
index 4c02b8d0..9c80b92d 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_AnuPpuccin_Table.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_AnuPpuccin_Table.approved.mermaid.md
@@ -1,34 +1,34 @@
 
 ```mermaid
 flowchart LR
-1[Unchecked]
-2[Checked]
-3[Rescheduled]
-4[Scheduled]
-5[Important]
-6[Cancelled]
-7[In Progress]
-8[Question]
-9[Star]
-10[Note]
-11[Location]
-12[Information]
-13[Idea]
-14[Amount]
-15[Pro]
-16[Con]
-17[Bookmark]
-18[Quote]
-19[Speech bubble 0]
-20[Speech bubble 1]
-21[Speech bubble 2]
-22[Speech bubble 3]
-23[Speech bubble 4]
-24[Speech bubble 5]
-25[Speech bubble 6]
-26[Speech bubble 7]
-27[Speech bubble 8]
-28[Speech bubble 9]
+1["Unchecked"]
+2["Checked"]
+3["Rescheduled"]
+4["Scheduled"]
+5["Important"]
+6["Cancelled"]
+7["In Progress"]
+8["Question"]
+9["Star"]
+10["Note"]
+11["Location"]
+12["Information"]
+13["Idea"]
+14["Amount"]
+15["Pro"]
+16["Con"]
+17["Bookmark"]
+18["Quote"]
+19["Speech bubble 0"]
+20["Speech bubble 1"]
+21["Speech bubble 2"]
+22["Speech bubble 3"]
+23["Speech bubble 4"]
+24["Speech bubble 5"]
+25["Speech bubble 6"]
+26["Speech bubble 7"]
+27["Speech bubble 8"]
+28["Speech bubble 9"]
 1 --> 2
 2 --> 1
 3 --> 2
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Aura_Table.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Aura_Table.approved.mermaid.md
index f8ca4e78..d9e32cad 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Aura_Table.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Aura_Table.approved.mermaid.md
@@ -1,25 +1,25 @@
 
 ```mermaid
 flowchart LR
-1[incomplete]
-2[complete / done]
-3[cancelled]
-4[deferred]
-5[in progress, or half-done]
-6[Important]
-7[question]
-8[review]
-9[Inbox / task that should be processed later]
-10[bookmark]
-11[brainstorm]
-12[deferred or scheduled]
-13[Info]
-14[idea]
-15[note]
-16[quote]
-17[win / success / reward]
-18[pro]
-19[con]
+1["incomplete"]
+2["complete / done"]
+3["cancelled"]
+4["deferred"]
+5["in progress, or half-done"]
+6["Important"]
+7["question"]
+8["review"]
+9["Inbox / task that should be processed later"]
+10["bookmark"]
+11["brainstorm"]
+12["deferred or scheduled"]
+13["Info"]
+14["idea"]
+15["note"]
+16["quote"]
+17["win / success / reward"]
+18["pro"]
+19["con"]
 1 --> 2
 2 --> 1
 3 --> 1
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Ebullientworks_Table.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Ebullientworks_Table.approved.mermaid.md
index 0d8ba1ac..afa32633 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Ebullientworks_Table.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Ebullientworks_Table.approved.mermaid.md
@@ -1,14 +1,14 @@
 
 ```mermaid
 flowchart LR
-1[Unchecked]
-2[Checked]
-3[Cancelled]
-4[In Progress]
-5[Deferred]
-6[Important]
-7[Question]
-8[Review]
+1["Unchecked"]
+2["Checked"]
+3["Cancelled"]
+4["In Progress"]
+5["Deferred"]
+6["Important"]
+7["Question"]
+8["Review"]
 1 --> 2
 2 --> 1
 3 --> 1
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_ITS_Table.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_ITS_Table.approved.mermaid.md
index 2fb248bb..b0e0e026 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_ITS_Table.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_ITS_Table.approved.mermaid.md
@@ -1,43 +1,43 @@
 
 ```mermaid
 flowchart LR
-1[Unchecked]
-2[Regular]
-3[Checked]
-4[Dropped]
-5[Forward]
-6[Date]
-7[Question]
-8[Half Done]
-9[Add]
-10[Research]
-11[Important]
-12[Idea]
-13[Brainstorm]
-14[Pro]
-15[Con]
-16[Quote]
-17[Note]
-18[Bookmark]
-19[Information]
-20[Paraphrase]
-21[Location]
-22[Example]
-23[Answer]
-24[Reward]
-25[Choice]
-26[Doing]
-27[Time]
-28[Character / Person]
-29[Talk]
-30[Outline / Plot]
-31[Conflict]
-32[World]
-33[Clue / Find]
-34[Foreshadow]
-35[Favorite / Health]
-36[Symbolism]
-37[Secret]
+1["Unchecked"]
+2["Regular"]
+3["Checked"]
+4["Dropped"]
+5["Forward"]
+6["Date"]
+7["Question"]
+8["Half Done"]
+9["Add"]
+10["Research"]
+11["Important"]
+12["Idea"]
+13["Brainstorm"]
+14["Pro"]
+15["Con"]
+16["Quote"]
+17["Note"]
+18["Bookmark"]
+19["Information"]
+20["Paraphrase"]
+21["Location"]
+22["Example"]
+23["Answer"]
+24["Reward"]
+25["Choice"]
+26["Doing"]
+27["Time"]
+28["Character / Person"]
+29["Talk"]
+30["Outline / Plot"]
+31["Conflict"]
+32["World"]
+33["Clue / Find"]
+34["Foreshadow"]
+35["Favorite / Health"]
+36["Symbolism"]
+37["Secret"]
 1 --> 2
 2 --> 1
 3 --> 1
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_LYT_Mode_Table.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_LYT_Mode_Table.approved.mermaid.md
index 921eba9c..5578738a 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_LYT_Mode_Table.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_LYT_Mode_Table.approved.mermaid.md
@@ -1,28 +1,28 @@
 
 ```mermaid
 flowchart LR
-1[Unchecked]
-2[Checked]
-3[Rescheduled]
-4[Scheduled]
-5[Important]
-6[Cancelled]
-7[In Progress]
-8[Question]
-9[Star]
-10[Note]
-11[Location]
-12[Information]
-13[Idea]
-14[Amount]
-15[Pro]
-16[Con]
-17[Bookmark]
-18[Fire]
-19[Key]
-20[Win]
-21[Up]
-22[Down]
+1["Unchecked"]
+2["Checked"]
+3["Rescheduled"]
+4["Scheduled"]
+5["Important"]
+6["Cancelled"]
+7["In Progress"]
+8["Question"]
+9["Star"]
+10["Note"]
+11["Location"]
+12["Information"]
+13["Idea"]
+14["Amount"]
+15["Pro"]
+16["Con"]
+17["Bookmark"]
+18["Fire"]
+19["Key"]
+20["Win"]
+21["Up"]
+22["Down"]
 1 --> 2
 2 --> 1
 3 --> 2
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Minimal_Table.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Minimal_Table.approved.mermaid.md
index 634931ae..8dfbdbec 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Minimal_Table.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Minimal_Table.approved.mermaid.md
@@ -1,28 +1,28 @@
 
 ```mermaid
 flowchart LR
-1[to-do]
-2[incomplete]
-3[done]
-4[canceled]
-5[forwarded]
-6[scheduling]
-7[question]
-8[important]
-9[star]
-10[quote]
-11[location]
-12[bookmark]
-13[information]
-14[savings]
-15[idea]
-16[pros]
-17[cons]
-18[fire]
-19[key]
-20[win]
-21[up]
-22[down]
+1["to-do"]
+2["incomplete"]
+3["done"]
+4["canceled"]
+5["forwarded"]
+6["scheduling"]
+7["question"]
+8["important"]
+9["star"]
+10["quote"]
+11["location"]
+12["bookmark"]
+13["information"]
+14["savings"]
+15["idea"]
+16["pros"]
+17["cons"]
+18["fire"]
+19["key"]
+20["win"]
+21["up"]
+22["down"]
 1 --> 3
 2 --> 3
 3 --> 1
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Things_Table.approved.mermaid.md b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Things_Table.approved.mermaid.md
index 634931ae..8dfbdbec 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Things_Table.approved.mermaid.md
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.Theme_Things_Table.approved.mermaid.md
@@ -1,28 +1,28 @@
 
 ```mermaid
 flowchart LR
-1[to-do]
-2[incomplete]
-3[done]
-4[canceled]
-5[forwarded]
-6[scheduling]
-7[question]
-8[important]
-9[star]
-10[quote]
-11[location]
-12[bookmark]
-13[information]
-14[savings]
-15[idea]
-16[pros]
-17[cons]
-18[fire]
-19[key]
-20[win]
-21[up]
-22[down]
+1["to-do"]
+2["incomplete"]
+3["done"]
+4["canceled"]
+5["forwarded"]
+6["scheduling"]
+7["question"]
+8["important"]
+9["star"]
+10["quote"]
+11["location"]
+12["bookmark"]
+13["information"]
+14["savings"]
+15["idea"]
+16["pros"]
+17["cons"]
+18["fire"]
+19["key"]
+20["win"]
+21["up"]
+22["down"]
 1 --> 3
 2 --> 3
 3 --> 1
diff --git a/tests/StatusRegistry.test.ts b/tests/StatusRegistry.test.ts
index 27144163..3c2e9ee6 100644
--- a/tests/StatusRegistry.test.ts
+++ b/tests/StatusRegistry.test.ts
@@ -173,10 +173,10 @@ describe('StatusRegistry', () => {
             "
             \`\`\`mermaid
             flowchart LR
-            1[Todo]
-            2[In Progress]
-            3[Done]
-            4[Cancelled]
+            1["Todo"]
+            2["In Progress"]
+            3["Done"]
+            4["Cancelled"]
             1 --> 3
             2 --> 3
             3 --> 1
@@ -203,6 +203,55 @@ describe('StatusRegistry', () => {
         `);
     });
 
+    it('should encode symbols in mermaid diagrams when necessary', () => {
+        // This tests the fix for:
+        //      https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2355
+        //      Fix the handling of special characters in Mermaid status diagrams
+
+        // Arrange
+        const statusRegistry = new StatusRegistry();
+        statusRegistry.clearStatuses();
+        statusRegistry.add(new StatusConfiguration('<', 'Todo <', '<', false, StatusType.TODO));
+        statusRegistry.add(new StatusConfiguration('>', 'Todo >', '>', false, StatusType.TODO));
+        statusRegistry.add(new StatusConfiguration('"', 'Todo "', '"', false, StatusType.TODO));
+        statusRegistry.add(new StatusConfiguration('&', 'Todo &', '&', false, StatusType.TODO));
+
+        // Assert
+        // Without detail:
+        expect(statusRegistry.mermaidDiagram(false)).toMatchInlineSnapshot(`
+            "
+            \`\`\`mermaid
+            flowchart LR
+            1["Todo &lt;"]
+            2["Todo &gt;"]
+            3["Todo &quot;"]
+            4["Todo &amp;"]
+            1 --> 1
+            2 --> 2
+            3 --> 3
+            4 --> 4
+            \`\`\`
+            "
+        `);
+
+        // With detail:
+        expect(statusRegistry.mermaidDiagram(true)).toMatchInlineSnapshot(`
+            "
+            \`\`\`mermaid
+            flowchart LR
+            1["'Todo &lt;'<br>[&lt;] -> [&lt;]<br>(TODO)"]
+            2["'Todo &gt;'<br>[&gt;] -> [&gt;]<br>(TODO)"]
+            3["'Todo &quot;'<br>[&quot;] -> [&quot;]<br>(TODO)"]
+            4["'Todo &amp;'<br>[&amp;] -> [&amp;]<br>(TODO)"]
+            1 --> 1
+            2 --> 2
+            3 --> 3
+            4 --> 4
+            \`\`\`
+            "
+        `);
+    });
+
     it('should not include unknown nextStatusSymbols in mermaid diagrams', () => {
         // Arrange
         const statusRegistry = new StatusRegistry();
@@ -220,7 +269,7 @@ describe('StatusRegistry', () => {
             "
             \`\`\`mermaid
             flowchart LR
-            1[Todo]
+            1["Todo"]
 
             \`\`\`
             "
diff --git a/tests/lib/HTMLCharacterEntities.test.ts b/tests/lib/HTMLCharacterEntities.test.ts
new file mode 100644
index 00000000..163fc4e4
--- /dev/null
+++ b/tests/lib/HTMLCharacterEntities.test.ts
@@ -0,0 +1,17 @@
+import { htmlEncodeCharacter, htmlEncodeString } from '../../src/lib/HTMLCharacterEntities';
+
+describe('HTMLEntities', () => {
+    it('should encode single characters', () => {
+        expect(htmlEncodeCharacter('a')).toEqual('a');
+        expect(htmlEncodeCharacter('<')).toEqual('&lt;');
+        expect(htmlEncodeCharacter('>')).toEqual('&gt;');
+        expect(htmlEncodeCharacter('&')).toEqual('&amp;');
+        expect(htmlEncodeCharacter('"')).toEqual('&quot;');
+    });
+
+    it('should encode multi-character strings', () => {
+        const input = 'This & that in <b>bold</b>';
+        const expected = 'This &amp; that in &lt;b&gt;bold&lt;/b&gt;';
+        expect(htmlEncodeString(input)).toEqual(expected);
+    });
+});