diff --git a/docs/dialects.md b/docs/dialects.md
index 36a1579..71c6264 100644
--- a/docs/dialects.md
+++ b/docs/dialects.md
@@ -114,11 +114,11 @@ flowchart TD
IN["filter text"] --> LEX["Lexer: tokens
(strings read as the dialect reads them)"]
LEX --> OP["an operator, as written
(e.g. !=, ILIKE, ~~, ISNULL)"]
OP --> D1{"does the dialect have
the operator?"}
- D1 -->|"no: ILIKE, ~ in SQL:2023 and MySQL 8.4;
SIMILAR TO, IS DISTINCT FROM in MySQL 8.4"| E1["operator "ILIKE" is not part of
the SQL:2023 dialect"]
+ D1 -->|"no: ILIKE, ~ in SQL:2023 and MySQL 8.4;
SIMILAR TO, IS DISTINCT FROM in MySQL 8.4"| E1["operator #quot;ILIKE#quot; is not part of
the SQL:2023 dialect"]
D1 -->|"yes"| D2{"does it have
this spelling of it?"}
- D2 -->|"no: != in SQL:2023; ~~ !~~ ISNULL NOTNULL
in both; BETWEEN SYMMETRIC in MySQL 8.4"| E2["!= is not part of the SQL:2023
dialect: write <>"]
+ D2 -->|"no: != in SQL:2023; ~~ !~~ ISNULL NOTNULL
in both; BETWEEN SYMMETRIC in MySQL 8.4"| E2["!= is not part of the SQL:2023
dialect: write #lt;#gt;"]
D2 -->|"yes"| A{"allowed by
WithAllowedOperators?"}
- A -->|"no"| E3["operator "LIKE" is not allowed"]
+ A -->|"no"| E3["operator #quot;LIKE#quot; is not allowed"]
A -->|"yes, or no allow-list"| OK["into the AST"]
E1 --> J["errors.Join: every problem"]
E2 --> J
diff --git a/docs/error-handling.md b/docs/error-handling.md
index b605142..ac27558 100644
--- a/docs/error-handling.md
+++ b/docs/error-handling.md
@@ -60,7 +60,7 @@ flowchart TD
P --> CONS{"entire input
consumed?"}
CONS -->|"no"| ADD["add 'unexpected token' error"]
CONS -->|"yes"| Q
- ADD --> Q{"len(errors) > 0?"}
+ ADD --> Q{"len(errors) #gt; 0?"}
Q -->|"yes"| J["return errors.Join(…)
of *QFVFilterError"]
Q -->|"no"| OK["return AST, nil"]
```
diff --git a/docs/filtering.md b/docs/filtering.md
index b3f95da..447f2b6 100644
--- a/docs/filtering.md
+++ b/docs/filtering.md
@@ -273,7 +273,7 @@ parses into this tree (precedence makes `AND` the root, with the parenthesized
graph TD
AND["BinaryOperatorNode
AND"]
AND --> GRP["GroupNode
( … )"]
- AND --> GT["BinaryOperatorNode
>"]
+ AND --> GT["BinaryOperatorNode
#gt;"]
GRP --> OR["BinaryOperatorNode
OR"]
OR --> EQ1["BinaryOperatorNode
="]
diff --git a/docs_mermaid_test.go b/docs_mermaid_test.go
new file mode 100644
index 0000000..27c21c3
--- /dev/null
+++ b/docs_mermaid_test.go
@@ -0,0 +1,50 @@
+package qfv
+
+import (
+ "os"
+ "path/filepath"
+ "regexp"
+ "testing"
+)
+
+// GitHub decodes HTML entities before it hands a diagram to Mermaid, so a
+// label written with " reaches Mermaid as a quote that ends the label:
+// docs/dialects.md's checking diagram rendered as a parse error on GitHub,
+// and as a diagram everywhere else. Mermaid's own entity codes (#quot;,
+// #lt;, #gt;, #amp;) are left alone by GitHub and decoded by Mermaid.
+func TestNoDiagramUsesAnHTMLEntity(t *testing.T) {
+ t.Parallel()
+
+ files, err := filepath.Glob("*.md")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ docs, err := filepath.Glob("docs/*.md")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ block := regexp.MustCompile("(?s)```mermaid\n(.*?)```")
+ entity := regexp.MustCompile(`&(quot|lt|gt|amp|#[0-9]+);`)
+ diagrams := 0
+
+ for _, f := range append(files, docs...) {
+ raw, err := os.ReadFile(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, m := range block.FindAllStringSubmatch(string(raw), -1) {
+ diagrams++
+
+ if e := entity.FindString(m[1]); e != "" {
+ t.Errorf("%s: a diagram uses the HTML entity %s, which GitHub decodes before Mermaid parses it: write #%s", f, e, e[1:])
+ }
+ }
+ }
+
+ if diagrams < 10 {
+ t.Fatalf("read %d diagrams, want at least 10: the docs moved and this test reads nothing", diagrams)
+ }
+}