1package text
2
3import (
4 "math"
5 "testing"
6
7 "github.com/go-text/typesetting/di"
8)
9
10func TestParagraphSeparators(t *testing.T) {
11 c := fixture(t)
12 e := NewEngine(c)
13 r := Request{Families: []string{"Noto Sans"}, Size: 20}
14 cases := []struct {
15 name, input string
16 paragraphs int
17 starts []int
18 empty []bool
19 dirs []di.Direction
20 }{
21 {"newline", "a\nb", 2, []int{0, 2}, []bool{false, false}, []di.Direction{di.DirectionLTR, di.DirectionLTR}},
22 {"rtl auto", "abc\nسلام", 2, []int{0, 4}, []bool{false, false}, []di.Direction{di.DirectionLTR, di.DirectionRTL}},
23 {"two empty", "\n\n", 3, nil, []bool{true, true, true}, []di.Direction{di.DirectionLTR, di.DirectionLTR, di.DirectionLTR}},
24 {"trailing newline", "a\n", 2, []int{0}, []bool{false, true}, []di.Direction{di.DirectionLTR, di.DirectionLTR}},
25 {"CRLF", "a\r\nسلام", 2, []int{0, 3}, []bool{false, false}, []di.Direction{di.DirectionLTR, di.DirectionRTL}},
26 {"separators", "a\rb\u001cc\u001dd\u001ee\u0085f\u2029g", 7, []int{0, 2, 4, 6, 8, 10, 12}, []bool{false, false, false, false, false, false, false}, nil},
27 }
28 for _, tc := range cases {
29 t.Run(tc.name, func(t *testing.T) {
30 l, err := e.Measure(tc.input, r, 0)
31 if err != nil {
32 t.Fatal(err)
33 }
34 if len(l.Lines) != tc.paragraphs {
35 t.Fatalf("lines=%d want %d", len(l.Lines), tc.paragraphs)
36 }
37 if math.Abs(l.Height-float64(len(l.Lines))*l.LineHeight) > 0.01 {
38 t.Errorf("height=%v lineHeight=%v", l.Height, l.LineHeight)
39 }
40 current := 0
41 for i, line := range l.Lines {
42 if i > 0 && line.Baseline <= l.Lines[i-1].Baseline {
43 t.Errorf("baseline not stacked")
44 }
45 if len(tc.dirs) > i && line.Direction != tc.dirs[i] {
46 t.Errorf("line %d direction %v want %v", i, line.Direction, tc.dirs[i])
47 }
48 if tc.empty[i] != (len(line.Runs) == 0) {
49 t.Errorf("line %d empty=%v", i, len(line.Runs) == 0)
50 }
51 if len(line.Runs) > 0 {
52 run := line.Runs[0]
53 if run.Start != tc.starts[current] {
54 t.Errorf("line %d start=%d want=%d", i, run.Start, tc.starts[current])
55 }
56 for _, rr := range line.Runs {
57 if rr.Start < 0 || rr.End > len([]rune(tc.input)) {
58 t.Errorf("global rune range %d:%d", rr.Start, rr.End)
59 }
60 for _, glyph := range rr.Glyphs {
61 if glyph.Cluster < rr.Start || glyph.Cluster >= rr.End {
62 t.Errorf("glyph cluster %d not global in %d:%d", glyph.Cluster, rr.Start, rr.End)
63 }
64 }
65 }
66 current++
67 }
68 }
69 })
70 }
71}
72
73// Offsets are rune indices into the original input, across multibyte text and
74// a CRLF separator, and every cluster of the later paragraph is covered.
75func TestParagraphClusterOffsets(t *testing.T) {
76 e := NewEngine(fixture(t))
77 in := "é日本\r\nxyz"
78 l, err := e.Measure(in, Request{Families: []string{"Noto Sans"}, Size: 20}, 0)
79 if err != nil {
80 t.Fatal(err)
81 }
82 if len(l.Lines) != 2 {
83 t.Fatalf("lines = %d, want 2", len(l.Lines))
84 }
85 runes := []rune(in)
86 seen := map[int]bool{}
87 for _, run := range l.Lines[1].Runs {
88 if run.Start < 5 || run.End > len(runes) {
89 t.Fatalf("run [%d,%d) outside second paragraph [5,%d)", run.Start, run.End, len(runes))
90 }
91 for _, g := range run.Glyphs {
92 seen[g.Cluster] = true
93 }
94 }
95 for i, want := range []rune("xyz") {
96 if !seen[5+i] || runes[5+i] != want {
97 t.Fatalf("cluster %d (%q) missing: %v", 5+i, want, seen)
98 }
99 }
100}