Move source files to root dir

This commit is contained in:
lash
2023-04-12 18:09:37 +01:00
parent e340210d8f
commit df9b30287c
42 changed files with 0 additions and 0 deletions

181
render/menu.go Normal file
View File

@@ -0,0 +1,181 @@
package render
import (
"fmt"
)
// BrowseError is raised when browsing outside the page range of a rendered node.
type BrowseError struct {
Idx uint16
PageCount uint16
}
// Error implements the Error interface.
func(err *BrowseError) Error() string {
return fmt.Sprintf("index is out of bounds: %v", err.Idx)
}
// BrowseConfig defines the availability and display parameters for page browsing.
type BrowseConfig struct {
NextAvailable bool
NextSelector string
NextTitle string
PreviousAvailable bool
PreviousSelector string
PreviousTitle string
}
// Default browse settings for convenience.
func DefaultBrowseConfig() BrowseConfig {
return BrowseConfig{
NextAvailable: true,
NextSelector: "11",
NextTitle: "next",
PreviousAvailable: true,
PreviousSelector: "22",
PreviousTitle: "previous",
}
}
// Menu renders menus. May be included in a Page object to render menus for pages.
type Menu struct {
menu [][2]string // selector and title for menu items.
browse BrowseConfig // browse definitions.
pageCount uint16 // number of pages the menu should represent.
canNext bool // availability flag for the "next" browse option.
canPrevious bool // availability flag for the "previous" browse option.
outputSize uint16 // maximum size constraint for the menu.
}
// NewMenu creates a new Menu with an explicit page count.
func NewMenu() *Menu {
return &Menu{}
}
// WithBrowseConfig defines the criteria for page browsing.
func(m *Menu) WithPageCount(pageCount uint16) *Menu {
m.pageCount = pageCount
return m
}
// WithSize defines the maximum byte size of the rendered menu.
func(m *Menu) WithOutputSize(outputSize uint16) *Menu {
m.outputSize = outputSize
return m
}
// GetOutputSize returns the defined heuristic menu size.
func(m *Menu) GetOutputSize() uint32 {
return uint32(m.outputSize)
}
// WithBrowseConfig defines the criteria for page browsing.
func(m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu {
m.browse = cfg
return m
}
// GetBrowseConfig returns a copy of the current state of the browse configuration.
func(m *Menu) GetBrowseConfig() BrowseConfig {
return m.browse
}
// Put adds a menu option to the menu rendering.
func(m *Menu) Put(selector string, title string) error {
m.menu = append(m.menu, [2]string{selector, title})
return nil
}
// ReservedSize returns the maximum render byte size of the menu.
func(m *Menu) ReservedSize() uint16 {
return m.outputSize
}
// Render returns the full current state of the menu as a string.
//
// After this has been executed, the state of the menu will be empty.
func(m *Menu) Render(idx uint16) (string, error) {
var menuCopy [][2]string
for _, v := range m.menu {
menuCopy = append(menuCopy, v)
}
err := m.applyPage(idx)
if err != nil {
return "", err
}
r := ""
for true {
l := len(r)
choice, title, err := m.shiftMenu()
if err != nil {
break
}
if l > 0 {
r += "\n"
}
r += fmt.Sprintf("%s:%s", choice, title)
}
m.menu = menuCopy
return r, nil
}
// add available browse options.
func(m *Menu) applyPage(idx uint16) error {
if m.pageCount == 0 {
if idx > 0 {
return fmt.Errorf("index %v > 0 for non-paged menu", idx)
}
return nil
} else if idx >= m.pageCount {
return &BrowseError{Idx: idx, PageCount: m.pageCount}
//return fmt.Errorf("index %v out of bounds (%v)", idx, m.pageCount)
}
m.reset()
if idx == m.pageCount - 1 {
m.canNext = false
}
if idx == 0 {
m.canPrevious = false
}
if m.canNext {
err := m.Put(m.browse.NextSelector, m.browse.NextTitle)
if err != nil {
return err
}
}
if m.canPrevious {
err := m.Put(m.browse.PreviousSelector, m.browse.PreviousTitle)
if err != nil {
return err
}
}
return nil
}
// removes and returns the first of remaining menu options.
// fails if menu is empty.
func(m *Menu) shiftMenu() (string, string, error) {
if len(m.menu) == 0 {
return "", "", fmt.Errorf("menu is empty")
}
r := m.menu[0]
m.menu = m.menu[1:]
return r[0], r[1], nil
}
// prepare menu object for re-use.
func(m *Menu) reset() {
if m.browse.NextAvailable {
m.canNext = true
}
if m.browse.PreviousAvailable {
m.canPrevious = true
}
}

84
render/menu_test.go Normal file
View File

@@ -0,0 +1,84 @@
package render
import (
"testing"
)
func TestMenuInit(t *testing.T) {
m := NewMenu()
err := m.Put("1", "foo")
if err != nil {
t.Fatal(err)
}
err = m.Put("2", "bar")
if err != nil {
t.Fatal(err)
}
r, err := m.Render(0)
if err != nil {
t.Fatal(err)
}
expect := `1:foo
2:bar`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
r, err = m.Render(1)
if err == nil {
t.Fatalf("expected render fail")
}
}
func TestMenuBrowse(t *testing.T) {
cfg := DefaultBrowseConfig()
m := NewMenu().WithPageCount(3).WithBrowseConfig(cfg)
err := m.Put("1", "foo")
if err != nil {
t.Fatal(err)
}
err = m.Put("2", "bar")
if err != nil {
t.Fatal(err)
}
r, err := m.Render(0)
if err != nil {
t.Fatal(err)
}
expect := `1:foo
2:bar
11:next`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
r, err = m.Render(1)
if err != nil {
t.Fatal(err)
}
expect = `1:foo
2:bar
11:next
22:previous`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
r, err = m.Render(2)
if err != nil {
t.Fatal(err)
}
expect = `1:foo
2:bar
22:previous`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
_, err = m.Render(3)
if err == nil {
t.Fatalf("expected render fail")
}
}

336
render/page.go Normal file
View File

@@ -0,0 +1,336 @@
package render
import (
"bytes"
"fmt"
"log"
"strings"
"text/template"
"git.defalsify.org/festive/cache"
"git.defalsify.org/festive/resource"
)
// Page exectues output rendering into pages constrained by size.
type Page struct {
cacheMap map[string]string // Mapped content symbols
cache cache.Memory // Content store.
resource resource.Resource // Symbol resolver.
menu *Menu // Menu rendererer.
sink *string // Content symbol rendered by dynamic size.
sizer *Sizer // Process size constraints.
}
// NewPage creates a new Page object.
func NewPage(cache cache.Memory, rs resource.Resource) *Page {
return &Page{
cache: cache,
cacheMap: make(map[string]string),
resource: rs,
}
}
// WithMenu sets a menu renderer for the page.
func(pg *Page) WithMenu(menu *Menu) *Page {
pg.menu = menu
if pg.sizer != nil {
pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize())
}
return pg
}
// WithSizer sets a size constraints definition for the page.
func(pg *Page) WithSizer(sizer *Sizer) *Page {
pg.sizer = sizer
if pg.menu != nil {
pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize())
}
return pg
}
// Usage returns size used by values and menu, and remaining size available
func(pg *Page) Usage() (uint32, uint32, error) {
var l int
var c uint16
for k, v := range pg.cacheMap {
l += len(v)
sz, err := pg.cache.ReservedSize(k)
if err != nil {
return 0, 0, err
}
c += sz
log.Printf("v %x %v %v %v %v", []byte(v), len(v), l, sz, c)
}
r := uint32(l)
rsv := uint32(c)-r
if pg.menu != nil {
r += uint32(pg.menu.ReservedSize())
}
return r, rsv, nil
}
// Map marks the given key for retrieval.
//
// After this, Val() will return the value for the key, and Size() will include the value size and limitations in its calculations.
//
// Only one symbol with no size limitation may be mapped at the current level.
func(pg *Page) Map(key string) error {
v, err := pg.cache.Get(key)
if err != nil {
return err
}
l, err := pg.cache.ReservedSize(key)
if err != nil {
return err
}
if l == 0 {
if pg.sink != nil && *pg.sink != key {
return fmt.Errorf("sink already set to symbol '%v'", *pg.sink)
}
pg.sink = &key
}
pg.cacheMap[key] = v
if pg.sizer != nil {
err := pg.sizer.Set(key, l)
if err != nil {
return err
}
}
log.Printf("mapped %s", key)
return nil
}
// Val gets the mapped content for the given symbol.
//
// Fails if key is not mapped.
func(pg *Page) Val(key string) (string, error) {
r := pg.cacheMap[key]
if len(r) == 0 {
return "", fmt.Errorf("key %v not mapped", key)
}
return r, nil
}
// Sizes returned the actual used bytes by each mapped symbol.
func(pg *Page) Sizes() (map[string]uint16, error) {
sizes := make(map[string]uint16)
var haveSink bool
for k, _ := range pg.cacheMap {
l, err := pg.cache.ReservedSize(k)
if err != nil {
return nil, err
}
if l == 0 {
if haveSink {
panic(fmt.Sprintf("duplicate sink for %v", k))
}
haveSink = true
}
}
return sizes, nil
}
// RenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate.
func(pg *Page) RenderTemplate(sym string, values map[string]string, idx uint16) (string, error) {
tpl, err := pg.resource.GetTemplate(sym)
if err != nil {
return "", err
}
if pg.sizer != nil {
values, err = pg.sizer.GetAt(values, idx)
if err != nil {
return "", err
}
} else if idx > 0 {
return "", fmt.Errorf("sizer needed for indexed render")
}
log.Printf("render for index: %v", idx)
tp, err := template.New("tester").Option("missingkey=error").Parse(tpl)
if err != nil {
return "", err
}
b := bytes.NewBuffer([]byte{})
err = tp.Execute(b, values)
if err != nil {
return "", err
}
return b.String(), err
}
// Render renders the current mapped content and menu state against the template associated with the symbol.
func(pg *Page) Render(sym string, idx uint16) (string, error) {
var err error
values, err := pg.prepare(sym, pg.cacheMap, idx)
if err != nil {
return "", err
}
return pg.render(sym, values, idx)
}
// Reset prepared the Page object for re-use.
//
// It clears mappings and removes the sink definition.
func(pg *Page) Reset() {
pg.sink = nil
pg.cacheMap = make(map[string]string)
}
// render menu and all syms except sink, split sink into display chunks
// TODO: Function too long, split up
func(pg *Page) prepare(sym string, values map[string]string, idx uint16) (map[string]string, error) {
var sink string
if pg.sizer == nil {
return values, nil
}
var sinkValues []string
noSinkValues := make(map[string]string)
for k, v := range values {
sz, err := pg.cache.ReservedSize(k)
if err != nil {
return nil, err
}
if sz == 0 {
sink = k
sinkValues = strings.Split(v, "\n")
v = ""
log.Printf("found sink %s with field count %v", k, len(sinkValues))
}
noSinkValues[k] = v
}
if sink == "" {
log.Printf("no sink found for sym %s", sym)
return values, nil
}
pg.sizer.AddCursor(0)
s, err := pg.render(sym, noSinkValues, 0)
if err != nil {
return nil, err
}
// remaining includes core menu
remaining, ok := pg.sizer.Check(s)
if !ok {
return nil, fmt.Errorf("capacity exceeded")
}
var menuSizes [4]uint32 // mainSize, prevsize, nextsize, nextsize+prevsize
if pg.menu != nil {
cfg := pg.menu.GetBrowseConfig()
tmpm := NewMenu().WithBrowseConfig(cfg)
v, err := tmpm.Render(0)
if err != nil {
return nil, err
}
menuSizes[0] = uint32(len(v))
tmpm = tmpm.WithPageCount(2)
v, err = tmpm.Render(0)
if err != nil {
return nil, err
}
menuSizes[1] = uint32(len(v)) - menuSizes[0]
v, err = tmpm.Render(1)
if err != nil {
return nil, err
}
menuSizes[2] = uint32(len(v)) - menuSizes[0]
menuSizes[3] = menuSizes[1] + menuSizes[2]
}
log.Printf("%v bytes available for sink split before navigation", remaining)
l := 0
var count uint16
tb := strings.Builder{}
rb := strings.Builder{}
netRemaining := remaining
if len(sinkValues) > 1 {
log.Printf("menusizes %v", menuSizes)
netRemaining -= menuSizes[1] - 1
}
for i, v := range sinkValues {
log.Printf("processing sinkvalue %v: %s", i, v)
l += len(v)
if uint32(l) > netRemaining {
if tb.Len() == 0 {
return nil, fmt.Errorf("capacity insufficient for sink field %v", i)
}
rb.WriteString(tb.String())
rb.WriteRune('\n')
c := uint32(rb.Len())
pg.sizer.AddCursor(c)
tb.Reset()
l = 0
if count == 0 {
netRemaining -= menuSizes[2]
}
count += 1
}
if tb.Len() > 0 {
tb.WriteByte(byte(0x00))
l += 1
}
tb.WriteString(v)
}
if tb.Len() > 0 {
rb.WriteString(tb.String())
count += 1
}
r := rb.String()
r = strings.TrimRight(r, "\n")
noSinkValues[sink] = r
if pg.menu != nil {
pg.menu = pg.menu.WithPageCount(count)
}
for i, v := range strings.Split(r, "\n") {
log.Printf("nosinkvalue %v: %s", i, v)
}
return noSinkValues, nil
}
// render template, menu (if it exists), and audit size constraint (if it exists).
func(pg *Page) render(sym string, values map[string]string, idx uint16) (string, error) {
var ok bool
r := ""
s, err := pg.RenderTemplate(sym, values, idx)
if err != nil {
return "", err
}
log.Printf("rendered %v bytes for template", len(s))
r += s
if pg.menu != nil {
s, err = pg.menu.Render(idx)
if err != nil {
return "", err
}
log.Printf("rendered %v bytes for menu", len(s))
if len(s) > 0 {
r += "\n" + s
}
}
if pg.sizer != nil {
_, ok = pg.sizer.Check(r)
if !ok {
return "", fmt.Errorf("limit exceeded: %v", pg.sizer)
}
}
return r, nil
}

115
render/page_test.go Normal file
View File

@@ -0,0 +1,115 @@
package render
import (
"testing"
"git.defalsify.org/festive/cache"
)
func TestPageCurrentSize(t *testing.T) {
t.Skip("usage is not in use, and it is unclear how it should be calculated")
ca := cache.NewCache()
pg := NewPage(ca, nil)
err := ca.Push()
if err != nil {
t.Error(err)
}
err = ca.Add("foo", "inky", 0)
if err != nil {
t.Error(err)
}
err = ca.Push()
pg.Reset()
err = ca.Add("bar", "pinky", 10)
if err != nil {
t.Error(err)
}
err = ca.Add("baz", "tinkywinkydipsylalapoo", 51)
if err != nil {
t.Error(err)
}
err = pg.Map("foo")
if err != nil {
t.Error(err)
}
err = pg.Map("bar")
if err != nil {
t.Error(err)
}
err = pg.Map("baz")
if err != nil {
t.Error(err)
}
l, c, err := pg.Usage()
if err != nil {
t.Error(err)
}
if l != 27 {
t.Errorf("expected actual length 27, got %v", l)
}
if c != 34 {
t.Errorf("expected remaining length 34, got %v", c)
}
mn := NewMenu().WithOutputSize(32)
pg = pg.WithMenu(mn)
l, c, err = pg.Usage()
if err != nil {
t.Error(err)
}
if l != 59 {
t.Errorf("expected actual length 59, got %v", l)
}
if c != 2 {
t.Errorf("expected remaining length 2, got %v", c)
}
}
func TestStateMapSink(t *testing.T) {
ca := cache.NewCache()
pg := NewPage(ca, nil)
ca.Push()
err := ca.Add("foo", "bar", 0)
if err != nil {
t.Error(err)
}
ca.Push()
pg.Reset()
err = ca.Add("bar", "xyzzy", 6)
if err != nil {
t.Error(err)
}
err = ca.Add("baz", "bazbaz", 18)
if err != nil {
t.Error(err)
}
err = ca.Add("xyzzy", "plugh", 0)
if err != nil {
t.Error(err)
}
err = pg.Map("foo")
if err != nil {
t.Error(err)
}
err = pg.Map("xyzzy")
if err == nil {
t.Errorf("Expected fail on duplicate sink")
}
err = pg.Map("baz")
if err != nil {
t.Error(err)
}
ca.Push()
pg.Reset()
err = pg.Map("foo")
if err != nil {
t.Error(err)
}
ca.Pop()
pg.Reset()
err = pg.Map("foo")
if err != nil {
t.Error(err)
}
}

113
render/size.go Normal file
View File

@@ -0,0 +1,113 @@
package render
import (
"bytes"
"fmt"
"log"
"strings"
)
// Sizer splits dynamic contents into individual segments for browseable pages.
type Sizer struct {
outputSize uint32 // maximum output for a single page.
menuSize uint16 // actual menu size for the dynamic page being sized
memberSizes map[string]uint16 // individual byte sizes of all content to be rendered by template.
totalMemberSize uint32 // total byte size of all content to be rendered by template (sum of memberSizes)
crsrs []uint32 // byte offsets in the sink content for browseable pages indices.
sink string // sink symbol.
}
// NewSizer creates a new Sizer object with the given output size constraint.
func NewSizer(outputSize uint32) *Sizer {
return &Sizer{
outputSize: outputSize,
memberSizes: make(map[string]uint16),
}
}
// WithMenuSize sets the size of the menu being used in the rendering context.
func(szr *Sizer) WithMenuSize(menuSize uint16) *Sizer {
szr.menuSize = menuSize
return szr
}
// Set adds a content symbol in the state it will be used by the renderer.
func(szr *Sizer) Set(key string, size uint16) error {
szr.memberSizes[key] = size
if size == 0 {
szr.sink = key
}
szr.totalMemberSize += uint32(size)
return nil
}
// Check audits whether the rendered string is within the output size constraint of the sizer.
func(szr *Sizer) Check(s string) (uint32, bool) {
l := uint32(len(s))
if szr.outputSize > 0 {
if l > szr.outputSize {
log.Printf("sizer check fails with length %v: %s", l, szr)
return 0, false
}
l = szr.outputSize - l
}
return l, true
}
// String implements the String interface.
func(szr *Sizer) String() string {
var diff uint32
if szr.outputSize > 0 {
diff = szr.outputSize - szr.totalMemberSize - uint32(szr.menuSize)
}
return fmt.Sprintf("output: %v, member: %v, menu: %v, diff: %v", szr.outputSize, szr.totalMemberSize, szr.menuSize, diff)
}
// Size gives the byte size of content for a single symbol.
//
// Fails if the symbol has not been registered using Set
func(szr *Sizer) Size(s string) (uint16, error) {
r, ok := szr.memberSizes[s]
if !ok {
return 0, fmt.Errorf("unknown member: %s", s)
}
return r, nil
}
// Menusize returns the currently defined menu size.
func(szr *Sizer) MenuSize() uint16 {
return szr.menuSize
}
// AddCursor adds a pagination cursor for the paged sink content.
func(szr *Sizer) AddCursor(c uint32) {
log.Printf("added cursor: %v", c)
szr.crsrs = append(szr.crsrs, c)
}
// GetAt the paged symbols for the current page index.
//
// Fails if index requested is out of range.
func(szr *Sizer) GetAt(values map[string]string, idx uint16) (map[string]string, error) {
if szr.sink == "" {
return values, nil
}
outValues := make(map[string]string)
for k, v := range values {
if szr.sink == k {
if idx >= uint16(len(szr.crsrs)) {
return nil, fmt.Errorf("no more values in index")
}
c := szr.crsrs[idx]
v = v[c:]
nl := strings.Index(v, "\n")
if nl > 0 {
v = v[:nl]
}
b := bytes.ReplaceAll([]byte(v), []byte{0x00}, []byte{0x0a})
v = string(b)
}
outValues[k] = v
}
return outValues, nil
}

186
render/size_test.go Normal file
View File

@@ -0,0 +1,186 @@
package render
import (
"context"
"fmt"
"testing"
"git.defalsify.org/festive/state"
"git.defalsify.org/festive/resource"
"git.defalsify.org/festive/cache"
)
type TestSizeResource struct {
*resource.MenuResource
}
func getTemplate(sym string) (string, error) {
var tpl string
switch sym {
case "small":
tpl = "one {{.foo}} two {{.bar}} three {{.baz}}"
case "toobig":
tpl = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus in mattis lorem. Aliquam erat volutpat. Ut vitae metus."
case "pages":
tpl = "one {{.foo}} two {{.bar}} three {{.baz}}\n{{.xyzzy}}"
}
return tpl, nil
}
func funcFor(sym string) (resource.EntryFunc, error) {
switch sym {
case "foo":
return getFoo, nil
case "bar":
return getBar, nil
case "baz":
return getBaz, nil
case "xyzzy":
return getXyzzy, nil
}
return nil, fmt.Errorf("unknown func: %s", sym)
}
func getFoo(sym string, ctx context.Context) (string, error) {
return "inky", nil
}
func getBar(sym string, ctx context.Context) (string, error) {
return "pinky", nil
}
func getBaz(sym string, ctx context.Context) (string, error) {
return "blinky", nil
}
func getXyzzy(sym string, ctx context.Context) (string, error) {
return "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", nil
}
func TestSizeCheck(t *testing.T) {
szr := NewSizer(16)
l, ok := szr.Check("foobar")
if !ok {
t.Fatalf("expected ok")
}
if l != 10 {
t.Fatalf("expected 10, got %v", l)
}
l, ok = szr.Check("inkypinkyblinkyclyde")
if ok {
t.Fatalf("expected not ok")
}
if l != 0 {
t.Fatalf("expected 0, got %v", l)
}
}
func TestSizeLimit(t *testing.T) {
st := state.NewState(0)
ca := cache.NewCache()
mn := NewMenu().WithOutputSize(32)
mrs := resource.NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate)
rs := TestSizeResource{
mrs,
}
szr := NewSizer(128)
pg := NewPage(ca, rs).WithMenu(mn).WithSizer(szr)
ca.Push()
st.Down("test")
err := ca.Add("foo", "inky", 4)
if err != nil {
t.Fatal(err)
}
err = ca.Add("bar", "pinky", 10)
if err != nil {
t.Fatal(err)
}
err = ca.Add("baz", "blinky", 0)
if err != nil {
t.Fatal(err)
}
err = pg.Map("foo")
if err != nil {
t.Fatal(err)
}
err = pg.Map("bar")
if err != nil {
t.Fatal(err)
}
err = pg.Map("baz")
if err != nil {
t.Fatal(err)
}
mn.Put("1", "foo the foo")
mn.Put("2", "go to bar")
_, err = pg.Render("small", 0)
if err != nil {
t.Fatal(err)
}
_, err = pg.Render("toobig", 0)
if err == nil {
t.Fatalf("expected size exceeded")
}
}
func TestSizePages(t *testing.T) {
st := state.NewState(0)
ca := cache.NewCache()
mn := NewMenu().WithOutputSize(32)
mrs := resource.NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate)
rs := TestSizeResource{
mrs,
}
szr := NewSizer(128)
pg := NewPage(ca, rs).WithSizer(szr).WithMenu(mn)
ca.Push()
st.Down("test")
ca.Add("foo", "inky", 4)
ca.Add("bar", "pinky", 10)
ca.Add("baz", "blinky", 20)
ca.Add("xyzzy", "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", 0)
pg.Map("foo")
pg.Map("bar")
pg.Map("baz")
pg.Map("xyzzy")
mn.Put("1", "foo the foo")
mn.Put("2", "go to bar")
r, err := pg.Render("pages", 0)
if err != nil {
t.Fatal(err)
}
expect := `one inky two pinky three blinky
inky pinky
blinky clyde sue
tinkywinky dipsy
lala poo
1:foo the foo
2:go to bar`
if r != expect {
t.Fatalf("expected:\n\t%x\ngot:\n\t%x\n", expect, r)
}
r, err = pg.Render("pages", 1)
if err != nil {
t.Fatal(err)
}
expect = `one inky two pinky three blinky
one two three four five six seven
eight nine ten
eleven twelve
1:foo the foo
2:go to bar`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
}