package markup
import (
"regexp"
"strings"
"testing"
)
func TestRenderPikchrSanitized(t *testing.T) {
out, err := RenderPikchr([]byte(`box "hello" fit; arrow right; circle "world" fit`))
if err != nil {
t.Fatal(err)
}
svg := string(out)
if !strings.Contains(svg, "<svg") || !strings.Contains(svg, "hello") {
t.Fatalf("svg missing content:\n%s", svg[:min(300, len(svg))])
}
// Geometry must survive the narrow policy — a diagram stripped of its
// paths would render blank while still "passing".
if !strings.Contains(svg, "<path") && !strings.Contains(svg, "<polygon") {
t.Fatalf("geometry stripped by policy:\n%s", svg[:min(400, len(svg))])
}
if !strings.Contains(svg, "style=") {
t.Fatalf("styling stripped, diagram would be invisible:\n%s", svg[:min(400, len(svg))])
}
}
func TestPikchrLabelCannotInject(t *testing.T) {
out, err := RenderPikchr([]byte(`box "</svg><script>alert(1)</script>" fit`))
if err != nil {
return // refusing to render is also an acceptable outcome
}
// The payload must survive only as escaped display text. Checking
// for the raw substring would false-positive on "<script>",
// which is exactly what correct escaping looks like.
if strings.Contains(string(out), "<script") || strings.Contains(string(out), "javascript:") {
t.Fatalf("unescaped script survived:\n%s", out)
}
if !strings.Contains(string(out), "<script>") {
t.Fatalf("payload neither escaped nor removed:\n%s", out)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// A ```pikchr fence inside markdown becomes an inline diagram, and the
// surrounding document still renders normally.
func TestPikchrFenceInMarkdown(t *testing.T) {
src := "Before.\n\n```pikchr\nbox \"fenced\" fit\n```\n\nAfter.\n"
out, err := Render([]byte(src), nil)
if err != nil {
t.Fatal(err)
}
got := string(out)
if !strings.Contains(got, "<svg") || !strings.Contains(got, "fenced") {
t.Fatalf("fence not rendered as svg:\n%s", got)
}
if !strings.Contains(got, "Before.") || !strings.Contains(got, "After.") {
t.Fatalf("surrounding markdown lost:\n%s", got)
}
// An ordinary code fence must still be a code block, not a diagram.
out, _ = Render([]byte("```go\nfmt.Println()\n```\n"), nil)
if strings.Contains(string(out), "<svg") {
t.Fatalf("non-pikchr fence rendered as a diagram:\n%s", out)
}
}
// A fence that will not compile shows pikchr's diagnostic, which quotes
// its source, never a broken page or a silent blank.
func TestPikchrFenceErrorShowsSource(t *testing.T) {
out, err := Render([]byte("```pikchr\nnot ( valid\n```\n"), nil)
if err != nil {
t.Fatal(err)
}
got := string(out)
if !strings.Contains(got, "not ( valid") || !strings.Contains(got, `class="pikchr-err"`) {
t.Fatalf("bad diagram did not degrade to source + message:\n%s", got)
}
}
// A diagram must be legible on both themes: quarry's pages follow
// prefers-color-scheme, so Pikchr's hardcoded black ink has to become
// currentColor or the whole diagram vanishes on the dark theme.
func TestPikchrInheritsThemeColor(t *testing.T) {
out, err := RenderPikchr([]byte(`box "visible" fit; arrow right; circle "here" fit`))
if err != nil {
t.Fatal(err)
}
got := string(out)
if strings.Contains(got, "rgb(0,0,0)") || strings.Contains(got, "rgb(0, 0, 0)") {
t.Fatalf("hardcoded black ink survives — invisible on the dark theme:\n%s", got[:min(400, len(got))])
}
if !strings.Contains(got, "currentColor") {
t.Fatalf("no currentColor: diagram will not follow the page theme:\n%s", got[:min(400, len(got))])
}
}
// Fossil reads words after the fence's language as the diagram's layout
// and allows a space before the language; both fences are diagrams.
func TestPikchrFenceTakesFossilWords(t *testing.T) {
for _, fence := range []string{"```pikchr toggle", "``` pikchr toggle", "```pikchr center", "```pikchr source"} {
out, err := Render([]byte("before\n\n"+fence+"\nbox \"x\" fit\n```\n\nafter\n"), nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(out), `class="pikchr-fig`) || strings.Contains(string(out), "language-pikchr") {
t.Errorf("FAIL: %q is not drawn:\n%s", fence, out)
}
}
out, _ := Render([]byte("```pikchr center\nbox fit\n```\n"), nil)
if !strings.Contains(string(out), "pikchr-center") {
t.Error("FAIL: center is not the figure's layout")
}
out, _ = Render([]byte("```pikchr source\nbox fit\n```\n"), nil)
if !strings.Contains(string(out), `pikchr-vs" name=`) || !regexp.MustCompile(`pikchr-vs"[^>]*checked`).MatchString(string(out)) {
t.Error("FAIL: source does not open on the source")
}
out, _ = Render([]byte("```pikchrish\nbox fit\n```\n"), nil)
if strings.Contains(string(out), "pikchr-fig") {
t.Error("FAIL: another language beginning with pikchr was drawn")
}
}
// A heading right under a line that is only <br /> is a heading, as on
// fossil; inside a code fence the lines are left alone.
func TestBreakLineEndsItsBlock(t *testing.T) {
out, err := Render([]byte("text\n\n<br />\n# A heading\n\nmore\n"), nil)
if err != nil {
t.Fatal(err)
}
if !regexp.MustCompile(`<h[1-6]`).Match(out) {
t.Errorf("FAIL: the heading under <br /> is not one:\n%s", out)
}
out, _ = Render([]byte("```\n<br />\n# not a heading\n```\n"), nil)
if regexp.MustCompile(`<h[1-6]`).Match(out) || strings.Contains(string(out), "\n\n# not") {
t.Errorf("FAIL: a fence's lines were changed:\n%s", out)
}
}
// A diagram pikchr cannot draw shows pikchr's own report, as on fossil:
// the line at fault and its ERROR line, not the module's exit status.
func TestPikchrErrorShowsItsDiagnostic(t *testing.T) {
out, err := Render([]byte("```pikchr\narc from (0.5,0.5) to (1.5,0) to (2,1)\n```\n"), nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(out), "ERROR:") || strings.Contains(string(out), "exit_code") {
t.Errorf("FAIL: the error is not pikchr's own:\n%s", out)
}
}