package markup
import (
"bytes"
"compress/flate"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"html"
"io"
"regexp"
"strings"
"testing"
)
// A DIAGRAM CARRIES ITS SOURCE, A COPY AND A WAY INTO THE STUDIO.
//
// A ```pikchr fence renders with a bar under it: a switch between the
// diagram and its source (radio inputs and labels, so it works with
// scripts off), the source itself, escaped, a Copy button, and a link
// that opens the same source in pikchr-studio.
func TestAFencedDiagramCarriesItsSourceAndStudioLink(t *testing.T) {
src := "box \"a</code><script>x()</script>\"; arrow; box \"b\""
out, err := Render([]byte("```pikchr\n"+src+"\n```\n"), nil)
if err != nil {
t.Fatal(err)
}
got := string(out)
for _, hook := range []string{`class="pikchr-v pikchr-vd"`, `class="pikchr-v pikchr-vs"`,
`class="pikchr-src"`, `class="pikchr-bar"`, `class="pikchr-act pikchr-copy"`, `class="pikchr-act pikchr-studio"`,
`data-on:click="`} {
if !strings.Contains(got, hook) {
t.Errorf("FAIL: the frame lacks %s", hook)
}
}
if strings.Contains(got, "<script>") {
t.Fatal("FAIL: the source was written into the page unescaped")
}
code := regexp.MustCompile(`(?s)<pre class="pikchr-src"><code>(.*?)</code></pre>`).FindStringSubmatch(got)
if code == nil || html.UnescapeString(code[1]) != src {
t.Fatalf("FAIL: the source shown is %q, want %q", code, src)
}
link := regexp.MustCompile(`class="pikchr-act pikchr-studio" href="([^"]+)"`).FindStringSubmatch(got)
if link == nil {
t.Fatal("FAIL: no studio link")
}
if back := studioSource(t, html.UnescapeString(link[1])); back != src {
t.Fatalf("FAIL: the studio link carries %q, want the source %q", back, src)
}
t.Log("ok: switch, escaped source, Copy, and a studio link that decodes to the source")
}
// A diagram that arrived pre-rendered has no source to show, so it has
// no bar; and a source too large for a URL keeps Copy but not the link.
func TestNoSourceNoBarAndTooLargeNoLink(t *testing.T) {
svg := []byte(`<svg class="pikchr" viewBox="0 0 100 50"><path d="M0,0"/></svg>`)
if strings.Contains(string(Figure(svg, nil)), "pikchr-bar") {
t.Error("FAIL: a diagram with no source grew a bar")
}
// Hashes do not compress, so this is a source no URL can carry.
var big strings.Builder
h := sha256.Sum256([]byte("seed"))
for big.Len() < 40000 {
h = sha256.Sum256(h[:])
big.WriteString("box \"" + hex.EncodeToString(h[:]) + "\"\n")
}
fig := string(Figure(svg, []byte(big.String())))
if !strings.Contains(fig, "pikchr-copy") || strings.Contains(fig, "pikchr-studio") {
t.Error("FAIL: a source too large for a URL should keep Copy and drop the studio link")
}
t.Log("ok: no source, no bar; too large, Copy only")
}
// studioSource decodes a studio link's #z= fragment back to the source.
func studioSource(t *testing.T, link string) string {
t.Helper()
if !strings.HasPrefix(link, StudioURL+"#z=") {
t.Fatalf("FAIL: the link %q is not the studio with a z= fragment", link)
}
raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(link, StudioURL+"#z="))
if err != nil {
t.Fatal(err)
}
b, err := io.ReadAll(flate.NewReader(bytes.NewReader(raw)))
if err != nil {
t.Fatal(err)
}
return string(b)
}