Move source files to root dir
This commit is contained in:
181
render/menu.go
Normal file
181
render/menu.go
Normal 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
84
render/menu_test.go
Normal 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
336
render/page.go
Normal 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
115
render/page_test.go
Normal 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
113
render/size.go
Normal 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
186
render/size_test.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user