text/unicod/bidi: implement API, remove panics

The bidi API splits strings with mixed left-to-right (ltr) and
right-to-left (rtl) parts into substrings (segments). Each segment
contains a substring, a direction of text flow (either ltr or rtl)
and the start and end positions in the input.

The paragraph validators do not panic, instead the newParagraph function
returns an error message in case the input is invalid.

Fixes golang/go#42356

Change-Id: I90cafc8fadb0cf6936dfb1ab373586017147d709
Reviewed-on: https://go-review.googlesource.com/c/text/+/267857
Trust: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/unicode/bidi/bidi.go b/unicode/bidi/bidi.go
index e8edc54..fd05760 100644
--- a/unicode/bidi/bidi.go
+++ b/unicode/bidi/bidi.go
@@ -12,15 +12,14 @@
 // and without notice.
 package bidi // import "golang.org/x/text/unicode/bidi"
 
-// TODO:
-// The following functionality would not be hard to implement, but hinges on
-// the definition of a Segmenter interface. For now this is up to the user.
-// - Iterate over paragraphs
-// - Segmenter to iterate over runs directly from a given text.
-// Also:
+// TODO
 // - Transformer for reordering?
 // - Transformer (validator, really) for Bidi Rule.
 
+import (
+	"bytes"
+)
+
 // This API tries to avoid dealing with embedding levels for now. Under the hood
 // these will be computed, but the question is to which extent the user should
 // know they exist. We should at some point allow the user to specify an
@@ -49,7 +48,9 @@
 	Neutral
 )
 
-type options struct{}
+type options struct {
+	defaultDirection Direction
+}
 
 // An Option is an option for Bidi processing.
 type Option func(*options)
@@ -66,12 +67,62 @@
 // DefaultDirection sets the default direction for a Paragraph. The direction is
 // overridden if the text contains directional characters.
 func DefaultDirection(d Direction) Option {
-	panic("unimplemented")
+	return func(opts *options) {
+		opts.defaultDirection = d
+	}
 }
 
 // A Paragraph holds a single Paragraph for Bidi processing.
 type Paragraph struct {
-	// buffers
+	p          []byte
+	o          Ordering
+	opts       []Option
+	types      []Class
+	pairTypes  []bracketType
+	pairValues []rune
+	runes      []rune
+	options    options
+}
+
+// Initialize the p.pairTypes, p.pairValues and p.types from the input previously
+// set by p.SetBytes() or p.SetString(). Also limit the input up to (and including) a paragraph
+// separator (bidi class B).
+//
+// The function p.Order() needs these values to be set, so this preparation could be postponed.
+// But since the SetBytes and SetStrings functions return the length of the input up to the paragraph
+// separator, the whole input needs to be processed anyway and should not be done twice.
+//
+// The function has the same return values as SetBytes() / SetString()
+func (p *Paragraph) prepareInput() (n int, err error) {
+	p.runes = bytes.Runes(p.p)
+	bytecount := 0
+	// clear slices from previous SetString or SetBytes
+	p.pairTypes = nil
+	p.pairValues = nil
+	p.types = nil
+
+	for _, r := range p.runes {
+		props, i := LookupRune(r)
+		bytecount += i
+		cls := props.Class()
+		if cls == B {
+			return bytecount, nil
+		}
+		p.types = append(p.types, cls)
+		if props.IsOpeningBracket() {
+			p.pairTypes = append(p.pairTypes, bpOpen)
+			p.pairValues = append(p.pairValues, r)
+		} else if props.IsBracket() {
+			// this must be a closing bracket,
+			// since IsOpeningBracket is not true
+			p.pairTypes = append(p.pairTypes, bpClose)
+			p.pairValues = append(p.pairValues, r)
+		} else {
+			p.pairTypes = append(p.pairTypes, bpNone)
+			p.pairValues = append(p.pairValues, 0)
+		}
+	}
+	return bytecount, nil
 }
 
 // SetBytes configures p for the given paragraph text. It replaces text
@@ -80,70 +131,150 @@
 // consumed from b including this separator. Error may be non-nil if options are
 // given.
 func (p *Paragraph) SetBytes(b []byte, opts ...Option) (n int, err error) {
-	panic("unimplemented")
+	p.p = b
+	p.opts = opts
+	return p.prepareInput()
 }
 
-// SetString configures p for the given paragraph text. It replaces text
-// previously set by SetBytes or SetString. If b contains a paragraph separator
+// SetString configures s for the given paragraph text. It replaces text
+// previously set by SetBytes or SetString. If s contains a paragraph separator
 // it will only process the first paragraph and report the number of bytes
-// consumed from b including this separator. Error may be non-nil if options are
+// consumed from s including this separator. Error may be non-nil if options are
 // given.
 func (p *Paragraph) SetString(s string, opts ...Option) (n int, err error) {
-	panic("unimplemented")
+	p.p = []byte(s)
+	p.opts = opts
+	return p.prepareInput()
 }
 
 // IsLeftToRight reports whether the principle direction of rendering for this
 // paragraphs is left-to-right. If this returns false, the principle direction
 // of rendering is right-to-left.
 func (p *Paragraph) IsLeftToRight() bool {
-	panic("unimplemented")
+	return p.Direction() == LeftToRight
 }
 
 // Direction returns the direction of the text of this paragraph.
 //
 // The direction may be LeftToRight, RightToLeft, Mixed, or Neutral.
 func (p *Paragraph) Direction() Direction {
-	panic("unimplemented")
+	return p.o.Direction()
 }
 
+// TODO: what happens if the position is > len(input)? This should return an error.
+
 // RunAt reports the Run at the given position of the input text.
 //
 // This method can be used for computing line breaks on paragraphs.
 func (p *Paragraph) RunAt(pos int) Run {
-	panic("unimplemented")
+	c := 0
+	runNumber := 0
+	for i, r := range p.o.runes {
+		c += len(r)
+		if pos < c {
+			runNumber = i
+		}
+	}
+	return p.o.Run(runNumber)
+}
+
+func calculateOrdering(levels []level, runes []rune) Ordering {
+	var curDir Direction
+
+	prevDir := Neutral
+	prevI := 0
+
+	o := Ordering{}
+	// lvl = 0,2,4,...: left to right
+	// lvl = 1,3,5,...: right to left
+	for i, lvl := range levels {
+		if lvl%2 == 0 {
+			curDir = LeftToRight
+		} else {
+			curDir = RightToLeft
+		}
+		if curDir != prevDir {
+			if i > 0 {
+				o.runes = append(o.runes, runes[prevI:i])
+				o.directions = append(o.directions, prevDir)
+				o.startpos = append(o.startpos, prevI)
+			}
+			prevI = i
+			prevDir = curDir
+		}
+	}
+	o.runes = append(o.runes, runes[prevI:])
+	o.directions = append(o.directions, prevDir)
+	o.startpos = append(o.startpos, prevI)
+	return o
 }
 
 // Order computes the visual ordering of all the runs in a Paragraph.
 func (p *Paragraph) Order() (Ordering, error) {
-	panic("unimplemented")
+	if len(p.types) == 0 {
+		return Ordering{}, nil
+	}
+
+	for _, fn := range p.opts {
+		fn(&p.options)
+	}
+	lvl := level(-1)
+	if p.options.defaultDirection == RightToLeft {
+		lvl = 1
+	}
+	para, err := newParagraph(p.types, p.pairTypes, p.pairValues, lvl)
+	if err != nil {
+		return Ordering{}, err
+	}
+
+	levels := para.getLevels([]int{len(p.types)})
+
+	p.o = calculateOrdering(levels, p.runes)
+	return p.o, nil
 }
 
 // Line computes the visual ordering of runs for a single line starting and
 // ending at the given positions in the original text.
 func (p *Paragraph) Line(start, end int) (Ordering, error) {
-	panic("unimplemented")
+	lineTypes := p.types[start:end]
+	para, err := newParagraph(lineTypes, p.pairTypes[start:end], p.pairValues[start:end], -1)
+	if err != nil {
+		return Ordering{}, err
+	}
+	levels := para.getLevels([]int{len(lineTypes)})
+	o := calculateOrdering(levels, p.runes[start:end])
+	return o, nil
 }
 
 // An Ordering holds the computed visual order of runs of a Paragraph. Calling
 // SetBytes or SetString on the originating Paragraph invalidates an Ordering.
 // The methods of an Ordering should only be called by one goroutine at a time.
-type Ordering struct{}
+type Ordering struct {
+	runes      [][]rune
+	directions []Direction
+	startpos   []int
+}
 
 // Direction reports the directionality of the runs.
 //
 // The direction may be LeftToRight, RightToLeft, Mixed, or Neutral.
 func (o *Ordering) Direction() Direction {
-	panic("unimplemented")
+	return o.directions[0]
 }
 
 // NumRuns returns the number of runs.
 func (o *Ordering) NumRuns() int {
-	panic("unimplemented")
+	return len(o.runes)
 }
 
 // Run returns the ith run within the ordering.
 func (o *Ordering) Run(i int) Run {
-	panic("unimplemented")
+	r := Run{
+		runes:     o.runes[i],
+		direction: o.directions[i],
+		startpos:  o.startpos[i],
+	}
+	return r
 }
 
 // TODO: perhaps with options.
@@ -155,16 +286,19 @@
 
 // A Run is a continuous sequence of characters of a single direction.
 type Run struct {
+	runes     []rune
+	direction Direction
+	startpos  int
 }
 
 // String returns the text of the run in its original order.
 func (r *Run) String() string {
-	panic("unimplemented")
+	return string(r.runes)
 }
 
 // Bytes returns the text of the run in its original order.
 func (r *Run) Bytes() []byte {
-	panic("unimplemented")
+	return []byte(r.String())
 }
 
 // TODO: methods for
@@ -174,25 +308,52 @@
 
 // Direction reports the direction of the run.
 func (r *Run) Direction() Direction {
-	panic("unimplemented")
+	return r.direction
 }
 
-// Position of the Run within the text passed to SetBytes or SetString of the
+// Pos returns the position of the Run within the text passed to SetBytes or SetString of the
 // originating Paragraph value.
 func (r *Run) Pos() (start, end int) {
-	panic("unimplemented")
+	return r.startpos, r.startpos + len(r.runes) - 1
 }
 
 // AppendReverse reverses the order of characters of in, appends them to out,
 // and returns the result. Modifiers will still follow the runes they modify.
 // Brackets are replaced with their counterparts.
 func AppendReverse(out, in []byte) []byte {
-	panic("unimplemented")
+	ret := make([]byte, len(in)+len(out))
+	copy(ret, out)
+	inRunes := bytes.Runes(in)
+
+	for i, r := range inRunes {
+		prop, _ := LookupRune(r)
+		if prop.IsBracket() {
+			inRunes[i] = prop.reverseBracket(r)
+		}
+	}
+
+	for i, j := 0, len(inRunes)-1; i < j; i, j = i+1, j-1 {
+		inRunes[i], inRunes[j] = inRunes[j], inRunes[i]
+	}
+	copy(ret[len(out):], string(inRunes))
+
+	return ret
 }
 
 // ReverseString reverses the order of characters in s and returns a new string.
 // Modifiers will still follow the runes they modify. Brackets are replaced with
 // their counterparts.
 func ReverseString(s string) string {
-	panic("unimplemented")
+	input := []rune(s)
+	li := len(input)
+	ret := make([]rune, li)
+	for i, r := range input {
+		prop, _ := LookupRune(r)
+		if prop.IsBracket() {
+			ret[li-i-1] = prop.reverseBracket(r)
+		} else {
+			ret[li-i-1] = r
+		}
+	}
+	return string(ret)
 }
diff --git a/unicode/bidi/bidi_test.go b/unicode/bidi/bidi_test.go
new file mode 100644
index 0000000..88572f5
--- /dev/null
+++ b/unicode/bidi/bidi_test.go
@@ -0,0 +1,347 @@
+package bidi
+
+import (
+	"log"
+	"testing"
+)
+
+type runInformation struct {
+	str   string
+	dir   Direction
+	start int
+	end   int
+}
+
+func TestSimple(t *testing.T) {
+	str := "Hellö"
+	p := Paragraph{}
+	p.SetString(str)
+	order, err := p.Order()
+	if err != nil {
+		log.Fatal(err)
+	}
+	expectedRuns := []runInformation{
+		{"Hellö", LeftToRight, 0, 4},
+	}
+
+	if !p.IsLeftToRight() {
+		t.Error("p.IsLeftToRight() == false; want true")
+	}
+	if nr, want := order.NumRuns(), len(expectedRuns); nr != want {
+		t.Errorf("order.NumRuns() = %d; want %d", nr, want)
+	}
+	for i, want := range expectedRuns {
+		r := order.Run(i)
+		if got := r.String(); got != want.str {
+			t.Errorf("Run(%d) = %q; want %q", i, got, want.str)
+		}
+		if s, e := r.Pos(); s != want.start || e != want.end {
+			t.Errorf("Run(%d).start = %d, .end = %d; want start %d, end %d", i, s, e, want.start, want.end)
+		}
+		if d := r.Direction(); d != want.dir {
+			t.Errorf("Run(%d).Direction = %d; want %d", i, d, want.dir)
+		}
+	}
+}
+
+func TestMixed(t *testing.T) {
+	str := `العاشر ليونيكود (Unicode Conference)، الذي سيعقد في 10-12 آذار 1997 مبدينة`
+	p := Paragraph{}
+	p.SetString(str)
+	order, err := p.Order()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if p.IsLeftToRight() {
+		t.Error("p.IsLeftToRight() == true; want false")
+	}
+
+	expectedRuns := []runInformation{
+		{"العاشر ليونيكود (", RightToLeft, 0, 16},
+		{"Unicode Conference", LeftToRight, 17, 34},
+		{")، الذي سيعقد في ", RightToLeft, 35, 51},
+		{"10", LeftToRight, 52, 53},
+		{"-", RightToLeft, 54, 54},
+		{"12", LeftToRight, 55, 56},
+		{" آذار ", RightToLeft, 57, 62},
+		{"1997", LeftToRight, 63, 66},
+		{" مبدينة", RightToLeft, 67, 73},
+	}
+
+	if nr, want := order.NumRuns(), len(expectedRuns); nr != want {
+		t.Errorf("order.NumRuns() = %d; want %d", nr, want)
+	}
+
+	for i, want := range expectedRuns {
+		r := order.Run(i)
+		if got := r.String(); got != want.str {
+			t.Errorf("Run(%d) = %q; want %q", i, got, want.str)
+		}
+		if s, e := r.Pos(); s != want.start || e != want.end {
+			t.Errorf("Run(%d).start = %d, .end = %d; want start = %d, end = %d", i, s, e, want.start, want.end)
+		}
+		if d := r.Direction(); d != want.dir {
+			t.Errorf("Run(%d).Direction = %d; want %d", i, d, want.dir)
+		}
+	}
+}
+
+func TestExplicitIsolate(t *testing.T) {
+	// https://www.w3.org/International/articles/inline-bidi-markup/uba-basics.en#beyond
+	str := "The names of these states in Arabic are \u2067مصر\u2069, \u2067البحرين\u2069 and \u2067الكويت\u2069 respectively."
+	p := Paragraph{}
+	p.SetString(str)
+	order, err := p.Order()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if !p.IsLeftToRight() {
+		t.Error("p.IsLeftToRight() == false; want true")
+	}
+
+	expectedRuns := []runInformation{
+		{"The names of these states in Arabic are \u2067", LeftToRight, 0, 40},
+		{"مصر", RightToLeft, 41, 43},
+		{"\u2069, \u2067", LeftToRight, 44, 47},
+		{"البحرين", RightToLeft, 48, 54},
+		{"\u2069 and \u2067", LeftToRight, 55, 61},
+		{"الكويت", RightToLeft, 62, 67},
+		{"\u2069 respectively.", LeftToRight, 68, 82},
+	}
+
+	if nr, want := order.NumRuns(), len(expectedRuns); nr != want {
+		t.Errorf("order.NumRuns() = %d; want %d", nr, want)
+	}
+
+	for i, want := range expectedRuns {
+		r := order.Run(i)
+		if got := r.String(); got != want.str {
+			t.Errorf("Run(%d) = %q; want %q", i, got, want.str)
+		}
+		if s, e := r.Pos(); s != want.start || e != want.end {
+			t.Errorf("Run(%d).start = %d, .end = %d; want start = %d, end = %d", i, s, e, want.start, want.end)
+		}
+		if d := r.Direction(); d != want.dir {
+			t.Errorf("Run(%d).Direction = %d; want %d", i, d, want.dir)
+		}
+	}
+}
+
+func TestWithoutExplicitIsolate(t *testing.T) {
+	str := "The names of these states in Arabic are مصر, البحرين and الكويت respectively."
+	p := Paragraph{}
+	p.SetString(str)
+	order, err := p.Order()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if !p.IsLeftToRight() {
+		t.Error("p.IsLeftToRight() == false; want true")
+	}
+
+	expectedRuns := []runInformation{
+		{"The names of these states in Arabic are ", LeftToRight, 0, 39},
+		{"مصر, البحرين", RightToLeft, 40, 51},
+		{" and ", LeftToRight, 52, 56},
+		{"الكويت", RightToLeft, 57, 62},
+		{" respectively.", LeftToRight, 63, 76},
+	}
+
+	if nr, want := order.NumRuns(), len(expectedRuns); nr != want {
+		t.Errorf("order.NumRuns() = %d; want %d", nr, want)
+	}
+
+	for i, want := range expectedRuns {
+		r := order.Run(i)
+		if got := r.String(); got != want.str {
+			t.Errorf("Run(%d) = %q; want %q", i, got, want.str)
+		}
+		if s, e := r.Pos(); s != want.start || e != want.end {
+			t.Errorf("Run(%d).start = %d, .end = %d; want start = %d, end = %d", i, s, e, want.start, want.end)
+		}
+		if d := r.Direction(); d != want.dir {
+			t.Errorf("Run(%d).Direction = %d; want %d", i, d, want.dir)
+		}
+	}
+}
+
+func TestLongUTF8(t *testing.T) {
+	str := `𠀀`
+	p := Paragraph{}
+	p.SetString(str)
+	order, err := p.Order()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if !p.IsLeftToRight() {
+		t.Error("p.IsLeftToRight() == false; want true")
+	}
+
+	expectedRuns := []runInformation{
+		{"𠀀", LeftToRight, 0, 0},
+	}
+
+	if nr, want := order.NumRuns(), len(expectedRuns); nr != want {
+		t.Errorf("order.NumRuns() = %d; want %d", nr, want)
+	}
+
+	for i, want := range expectedRuns {
+		r := order.Run(i)
+		if got := r.String(); got != want.str {
+			t.Errorf("Run(%d) = %q; want %q", i, got, want.str)
+		}
+		if s, e := r.Pos(); s != want.start || e != want.end {
+			t.Errorf("Run(%d).start = %d, .end = %d; want start = %d, end = %d", i, s, e, want.start, want.end)
+		}
+		if d := r.Direction(); d != want.dir {
+			t.Errorf("Run(%d).Direction = %d; want %d", i, d, want.dir)
+		}
+	}
+}
+
+func TestLLongUTF8(t *testing.T) {
+	strTester := []struct {
+		str string
+		l   int
+	}{
+		{"ö", 2},
+		{"ॡ", 3},
+		{`𠀀`, 4},
+	}
+	for _, st := range strTester {
+		str := st.str
+		want := st.l
+		if _, l := LookupString(str); l != want {
+			t.Errorf("LookupString(%q) length = %d; want %d", str, l, want)
+		}
+
+	}
+
+}
+
+func TestMixedSimple(t *testing.T) {
+	str := `Uا`
+	p := Paragraph{}
+	p.SetString(str)
+	order, err := p.Order()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if !p.IsLeftToRight() {
+		t.Error("p.IsLeftToRight() == false; want true")
+	}
+
+	expectedRuns := []runInformation{
+		{"U", LeftToRight, 0, 0},
+		{"ا", RightToLeft, 1, 1},
+	}
+
+	if nr, want := order.NumRuns(), len(expectedRuns); nr != want {
+		t.Errorf("order.NumRuns() = %d; want %d", nr, want)
+	}
+
+	for i, want := range expectedRuns {
+		r := order.Run(i)
+		if got := r.String(); got != want.str {
+			t.Errorf("Run(%d) = %q; want %q", i, got, want.str)
+		}
+		if s, e := r.Pos(); s != want.start || e != want.end {
+			t.Errorf("Run(%d).start = %d, .end = %d; want start = %d, end = %d", i, s, e, want.start, want.end)
+		}
+		if d := r.Direction(); d != want.dir {
+			t.Errorf("Run(%d).Direction = %d; want %d", i, d, want.dir)
+		}
+	}
+}
+
+func TestDefaultDirection(t *testing.T) {
+	str := "+"
+	p := Paragraph{}
+	p.SetString(str, DefaultDirection(RightToLeft))
+	_, err := p.Order()
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+	if want, dir := false, p.IsLeftToRight(); want != dir {
+		t.Errorf("p.IsLeftToRight() = %t; want %t", dir, want)
+	}
+	p.SetString(str, DefaultDirection(LeftToRight))
+	_, err = p.Order()
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+	if want, dir := true, p.IsLeftToRight(); want != dir {
+		t.Errorf("p.IsLeftToRight() = %t; want %t", dir, want)
+	}
+
+}
+
+func TestEmpty(t *testing.T) {
+	p := Paragraph{}
+	p.SetBytes([]byte{})
+	o, err := p.Order()
+	if err != nil {
+		t.Error("p.Order() return err != nil; want err == nil")
+	}
+	if nr := o.NumRuns(); nr != 0 {
+		t.Errorf("o.NumRuns() = %d; want 0", nr)
+	}
+}
+
+func TestNewline(t *testing.T) {
+	str := "Hello\nworld"
+	p := Paragraph{}
+	n, err := p.SetString(str)
+	if err != nil {
+		t.Error(err)
+	}
+	// 6 is the length up to and including the \n
+	if want := 6; n != want {
+		t.Errorf("SetString(%q) = nil, %d; want nil, %d", str, n, want)
+	}
+}
+
+func TestDoubleSetString(t *testing.T) {
+	str := "العاشر ليونيكود (Unicode Conference)،"
+	p := Paragraph{}
+	_, err := p.SetString(str)
+	if err != nil {
+		t.Error(err)
+	}
+	_, err = p.SetString(str)
+	if err != nil {
+		t.Error(err)
+	}
+	_, err = p.Order()
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestReverseString(t *testing.T) {
+	input := "(Hello)"
+	want := "(olleH)"
+	if str := ReverseString(input); str != want {
+		t.Errorf("ReverseString(%s) = %q; want %q", input, str, want)
+	}
+}
+
+func TestAppendReverse(t *testing.T) {
+	testcase := []struct {
+		inString  string
+		outString string
+		want      string
+	}{
+		{"", "Hëllo", "Hëllo"},
+		{"nice (wörld)", "", "(dlröw) ecin"},
+		{"nice (wörld)", "Hëllo", "Hëllo(dlröw) ecin"},
+	}
+	for _, tc := range testcase {
+		if r := AppendReverse([]byte(tc.outString), []byte(tc.inString)); string(r) != tc.want {
+			t.Errorf("AppendReverse([]byte(%q), []byte(%q) = %q; want %q", tc.outString, tc.inString, string(r), tc.want)
+		}
+	}
+
+}
diff --git a/unicode/bidi/core.go b/unicode/bidi/core.go
index 50deb66..e4c0811 100644
--- a/unicode/bidi/core.go
+++ b/unicode/bidi/core.go
@@ -4,7 +4,10 @@
 
 package bidi
 
-import "log"
+import (
+	"fmt"
+	"log"
+)
 
 // This implementation is a port based on the reference implementation found at:
 // https://www.unicode.org/Public/PROGRAMS/BidiReferenceJava/
@@ -97,13 +100,20 @@
 // rune (suggested is the rune of the open bracket for opening and matching
 // close brackets, after normalization). The embedding levels are optional, but
 // may be supplied to encode embedding levels of styled text.
-//
-// TODO: return an error.
-func newParagraph(types []Class, pairTypes []bracketType, pairValues []rune, levels level) *paragraph {
-	validateTypes(types)
-	validatePbTypes(pairTypes)
-	validatePbValues(pairValues, pairTypes)
-	validateParagraphEmbeddingLevel(levels)
+func newParagraph(types []Class, pairTypes []bracketType, pairValues []rune, levels level) (*paragraph, error) {
+	var err error
+	if err = validateTypes(types); err != nil {
+		return nil, err
+	}
+	if err = validatePbTypes(pairTypes); err != nil {
+		return nil, err
+	}
+	if err = validatePbValues(pairValues, pairTypes); err != nil {
+		return nil, err
+	}
+	if err = validateParagraphEmbeddingLevel(levels); err != nil {
+		return nil, err
+	}
 
 	p := &paragraph{
 		initialTypes:   append([]Class(nil), types...),
@@ -115,7 +125,7 @@
 		resultTypes: append([]Class(nil), types...),
 	}
 	p.run()
-	return p
+	return p, nil
 }
 
 func (p *paragraph) Len() int { return len(p.initialTypes) }
@@ -1001,58 +1011,61 @@
 	return R
 }
 
-// TODO: change validation to not panic
-
-func validateTypes(types []Class) {
+func validateTypes(types []Class) error {
 	if len(types) == 0 {
-		log.Panic("types is null")
+		return fmt.Errorf("types is null")
 	}
 	for i, t := range types[:len(types)-1] {
 		if t == B {
-			log.Panicf("B type before end of paragraph at index: %d", i)
+			return fmt.Errorf("B type before end of paragraph at index: %d", i)
 		}
 	}
+	return nil
 }
 
-func validateParagraphEmbeddingLevel(embeddingLevel level) {
+func validateParagraphEmbeddingLevel(embeddingLevel level) error {
 	if embeddingLevel != implicitLevel &&
 		embeddingLevel != 0 &&
 		embeddingLevel != 1 {
-		log.Panicf("illegal paragraph embedding level: %d", embeddingLevel)
+		return fmt.Errorf("illegal paragraph embedding level: %d", embeddingLevel)
 	}
+	return nil
 }
 
-func validateLineBreaks(linebreaks []int, textLength int) {
+func validateLineBreaks(linebreaks []int, textLength int) error {
 	prev := 0
 	for i, next := range linebreaks {
 		if next <= prev {
-			log.Panicf("bad linebreak: %d at index: %d", next, i)
+			return fmt.Errorf("bad linebreak: %d at index: %d", next, i)
 		}
 		prev = next
 	}
 	if prev != textLength {
-		log.Panicf("last linebreak was %d, want %d", prev, textLength)
+		return fmt.Errorf("last linebreak was %d, want %d", prev, textLength)
 	}
+	return nil
 }
 
-func validatePbTypes(pairTypes []bracketType) {
+func validatePbTypes(pairTypes []bracketType) error {
 	if len(pairTypes) == 0 {
-		log.Panic("pairTypes is null")
+		return fmt.Errorf("pairTypes is null")
 	}
 	for i, pt := range pairTypes {
 		switch pt {
 		case bpNone, bpOpen, bpClose:
 		default:
-			log.Panicf("illegal pairType value at %d: %v", i, pairTypes[i])
+			return fmt.Errorf("illegal pairType value at %d: %v", i, pairTypes[i])
 		}
 	}
+	return nil
 }
 
-func validatePbValues(pairValues []rune, pairTypes []bracketType) {
+func validatePbValues(pairValues []rune, pairTypes []bracketType) error {
 	if pairValues == nil {
-		log.Panic("pairValues is null")
+		return fmt.Errorf("pairValues is null")
 	}
 	if len(pairTypes) != len(pairValues) {
-		log.Panic("pairTypes is different length from pairValues")
+		return fmt.Errorf("pairTypes is different length from pairValues")
 	}
+	return nil
 }
diff --git a/unicode/bidi/core_test.go b/unicode/bidi/core_test.go
index b653399..1c928af 100644
--- a/unicode/bidi/core_test.go
+++ b/unicode/bidi/core_test.go
@@ -55,7 +55,10 @@
 				continue
 			}
 			lev := level(int(i) - 1)
-			par := newParagraph(types, pairTypes, pairValues, lev)
+			par, err := newParagraph(types, pairTypes, pairValues, lev)
+			if err != nil {
+				t.Error(err)
+			}
 
 			if *testLevels {
 				levels := par.getLevels([]int{len(types)})
@@ -142,7 +145,10 @@
 				pairValues = append(pairValues, p.reverseBracket(r))
 			}
 		}
-		par := newParagraph(types, pairTypes, pairValues, parLevel)
+		par, err := newParagraph(types, pairTypes, pairValues, parLevel)
+		if err != nil {
+			t.Error(err)
+		}
 
 		// Test results:
 		if got := par.embeddingLevel; got != wantLevel {