package markup import ( "bytes" "strings" "testing" "tangled.org/core/appview/pages/markup/sanitizer" ) func TestMermaidExtension(t *testing.T) { tests := []struct { name string markdown string contains string notContains string }{ { name: "mermaid block produces pre.mermaid", markdown: "```mermaid\ngraph TD\n A-->B\n```", contains: `
`,
notContains: `B\n```",
contains: "graph TD",
},
{
name: "non-mermaid code block is not affected",
markdown: "```go\nfunc main() {}\n```",
contains: ``,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
md := NewMarkdown("tangled.org")
var buf bytes.Buffer
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
t.Fatalf("failed to convert markdown: %v", err)
}
result := buf.String()
if !strings.Contains(result, tt.contains) {
t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
}
if tt.notContains != "" && strings.Contains(result, tt.notContains) {
t.Errorf("expected output NOT to contain:\n%s\ngot:\n%s", tt.notContains, result)
}
})
}
}
func TestMathExtension(t *testing.T) {
tests := []struct {
name string
markdown string
contains string
notContains string
}{
{
name: "inline math produces span with mathjax delimiters",
markdown: "the famous $E = mc^2$ equation",
contains: `\(E = mc^2\)`,
},
{
name: "block math produces display span",
markdown: "$$\n\\frac{a}{b}\n$$",
contains: `\[`,
},
{
name: "underscores inside math are not treated as emphasis",
markdown: "$a_1 + a_2$",
contains: `\(a_1 + a_2\)`,
notContains: "",
},
{
name: "non-math dollar usage is left alone",
markdown: "it costs $5 today",
notContains: `class="math`,
},
{
// regression: two currency amounts must not be parsed as one
// inline math span (the "$5 and $" .. "10" case).
name: "currency pair is not math",
markdown: "it costs $5 today and $10 tomorrow",
contains: "it costs $5 today and $10 tomorrow",
notContains: `class="math`,
},
{
// regression: single-line $$...$$ must keep both the math and the
// trailing prose.
name: "single-line block keeps trailing prose",
markdown: "$$x^2$$ and then prose",
contains: "and then prose",
},
{
// math content with < / & must be escaped so the sanitizer keeps
// the span and MathJax reads the literal source.
name: "angle brackets in math are escaped",
markdown: "$a < b$",
contains: `\(a < b\)`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
md := NewMarkdown("tangled.org")
var buf bytes.Buffer
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
t.Fatalf("failed to convert markdown: %v", err)
}
result := buf.String()
if tt.contains != "" && !strings.Contains(result, tt.contains) {
t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
}
if tt.notContains != "" && strings.Contains(result, tt.notContains) {
t.Errorf("expected output NOT to contain:\n%s\ngot:\n%s", tt.notContains, result)
}
})
}
}
// The sanitizer must preserve the carrier spans that MathJax renders client-side.
func TestMathSurvivesSanitizer(t *testing.T) {
md := NewMarkdown("tangled.org")
var buf bytes.Buffer
if err := md.Convert([]byte("inline $x^2$ and block\n\n$$\ny^2\n$$"), &buf); err != nil {
t.Fatalf("failed to convert markdown: %v", err)
}
out := sanitizer.SanitizeDefault(buf.String())
for _, want := range []string{`class="math inline"`, `class="math display"`} {
if !strings.Contains(out, want) {
t.Errorf("sanitizer stripped math span; expected %q in:\n%s", want, out)
}
}
}
func TestAtExtension_Rendering(t *testing.T) {
tests := []struct {
name string
markdown string
expected string
}{
{
name: "renders simple at mention",
markdown: "Hello @user.tngl.sh!",
expected: `Hello @user.tngl.sh!
`,
},
{
name: "renders multiple at mentions",
markdown: "Hi @alice.tngl.sh and @bob.example.com",
expected: `Hi @alice.tngl.sh and @bob.example.com
`,
},
{
name: "renders at mention in parentheses",
markdown: "Check this out (@user.tngl.sh)",
expected: `Check this out (@user.tngl.sh)
`,
},
{
name: "does not render email",
markdown: "Contact me at test@example.com",
expected: `Contact me at test@example.com
`,
},
{
name: "renders at mention with hyphen",
markdown: "Follow @user-name.tngl.sh",
expected: `Follow @user-name.tngl.sh
`,
},
{
name: "renders at mention with numbers",
markdown: "@user123.test456.social",
expected: ``,
},
{
name: "at mention at start of line",
markdown: "@user.tngl.sh is cool",
expected: `@user.tngl.sh is cool
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
md := NewMarkdown("tangled.org")
var buf bytes.Buffer
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
t.Fatalf("failed to convert markdown: %v", err)
}
result := buf.String()
if result != tt.expected+"\n" {
t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, result)
}
})
}
}
func TestAtExtension_WithOtherMarkdown(t *testing.T) {
tests := []struct {
name string
markdown string
contains string
}{
{
name: "at mention with bold",
markdown: "**Hello @user.tngl.sh**",
contains: `Hello @user.tngl.sh`,
},
{
name: "at mention with italic",
markdown: "*Check @user.tngl.sh*",
contains: `Check @user.tngl.sh`,
},
{
name: "at mention in list",
markdown: "- Item 1\n- @user.tngl.sh\n- Item 3",
contains: `@user.tngl.sh`,
},
{
name: "at mention in link",
markdown: "[@regnault.dev](https://regnault.dev)",
contains: `@regnault.dev`,
},
{
name: "at mention in link again",
markdown: "[check out @regnault.dev](https://regnault.dev)",
contains: `check out @regnault.dev`,
},
{
name: "at mention in link again, multiline",
markdown: "[\ncheck out @regnault.dev](https://regnault.dev)",
contains: "\ncheck out @regnault.dev",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
md := NewMarkdown("tangled.org")
var buf bytes.Buffer
if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
t.Fatalf("failed to convert markdown: %v", err)
}
result := buf.String()
if !bytes.Contains([]byte(result), []byte(tt.contains)) {
t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
}
})
}
}