go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

page.go (9985B)


      1 package render
      2 
      3 import (
      4 	"bytes"
      5 	"context"
      6 	"fmt"
      7 	"strings"
      8 	"text/template"
      9 
     10 	"git.defalsify.org/vise.git/cache"
     11 	"git.defalsify.org/vise.git/resource"
     12 )
     13 
     14 // Page exectues output rendering into pages constrained by size.
     15 type Page struct {
     16 	cacheMap map[string]string // Mapped content symbols
     17 	cache cache.Memory // Content store.
     18 	resource resource.Resource // Symbol resolver.
     19 	menu *Menu // Menu rendererer.
     20 	sink *string // Content symbol rendered by dynamic size.
     21 	sizer *Sizer // Process size constraints.
     22 	err error // Error state to prepend to output.
     23 	extra string // Extra content to append to received template
     24 }
     25 
     26 // NewPage creates a new Page object.
     27 func NewPage(cache cache.Memory, rs resource.Resource) *Page {
     28 	return &Page{
     29 		cache: cache,
     30 		cacheMap: make(map[string]string),
     31 		resource: rs,
     32 	}
     33 }
     34 
     35 // WithMenu sets a menu renderer for the page.
     36 func(pg *Page) WithMenu(menu *Menu) *Page {
     37 	pg.menu = menu.WithResource(pg.resource)
     38 	//if pg.sizer != nil {
     39 	//	pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize())
     40 	//}
     41 	return pg
     42 }
     43 
     44 // WithSizer sets a size constraints definition for the page.
     45 func(pg *Page) WithSizer(sizer *Sizer) *Page {
     46 	pg.sizer = sizer
     47 	//if pg.menu != nil {
     48 		//pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize())
     49 	//}
     50 	return pg
     51 }
     52 
     53 // WithError adds an error to prepend to the page output.
     54 func(pg *Page) WithError(err error) *Page {
     55 	pg.err = err
     56 	return pg
     57 }
     58 
     59 // Error implements error interface.
     60 func(pg *Page) Error() string {
     61 	if pg.err != nil {
     62 		return pg.err.Error()
     63 	}
     64 	return ""
     65 }
     66 
     67 // Usage returns size used by values and menu, and remaining size available
     68 func(pg *Page) Usage() (uint32, uint32, error) {
     69 	var l int
     70 	var c uint16
     71 	for k, v := range pg.cacheMap {
     72 		l += len(v)
     73 		sz, err := pg.cache.ReservedSize(k)
     74 		if err != nil {
     75 			return 0, 0, err
     76 		}
     77 		c += sz
     78 	}
     79 	r := uint32(l)
     80 	rsv := uint32(c)-r
     81 	//if pg.menu != nil {
     82 	//	r += uint32(pg.menu.ReservedSize())
     83 	//}
     84 	return r, rsv, nil
     85 }
     86 
     87 // Map marks the given key for retrieval.
     88 //
     89 // After this, Val() will return the value for the key, and Size() will include the value size and limitations in its calculations.
     90 //
     91 // Only one symbol with no size limitation may be mapped at the current level.
     92 func(pg *Page) Map(key string) error {
     93 	v, err := pg.cache.Get(key)
     94 	if err != nil {
     95 		return err
     96 	}
     97 	l, err := pg.cache.ReservedSize(key)
     98 	if err != nil {
     99 		return err
    100 	}
    101 	if l == 0 {
    102 		if pg.sink != nil && *pg.sink != key {
    103 			return fmt.Errorf("sink already set to symbol '%v'", *pg.sink)
    104 		}
    105 		pg.sink = &key
    106 	}
    107 	pg.cacheMap[key] = v
    108 	if pg.sizer != nil {
    109 		err := pg.sizer.Set(key, l)
    110 		if err != nil {
    111 			return err
    112 		}
    113 	}
    114 	Logg.Tracef("mapped", "key", key)
    115 	return nil
    116 }
    117 
    118 // Val gets the mapped content for the given symbol.
    119 //
    120 // Fails if key is not mapped.
    121 func(pg *Page) Val(key string) (string, error) {
    122 	r := pg.cacheMap[key]
    123 	if len(r) == 0 {
    124 		return "", fmt.Errorf("key %v not mapped", key)
    125 	}
    126 	return r, nil
    127 }
    128 
    129 // Sizes returned the actual used bytes by each mapped symbol.
    130 func(pg *Page) Sizes() (map[string]uint16, error) {
    131 	sizes := make(map[string]uint16)
    132 	var haveSink bool
    133 	for k, _ := range pg.cacheMap {
    134 		l, err := pg.cache.ReservedSize(k)
    135 		if err != nil {
    136 			return nil, err
    137 		}
    138 		if l == 0 {
    139 			if haveSink {
    140 				panic(fmt.Sprintf("duplicate sink for %v", k))
    141 			}
    142 			haveSink = true
    143 		}
    144 	}
    145 	return sizes, nil
    146 }
    147 
    148 // RenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate.
    149 func(pg *Page) RenderTemplate(ctx context.Context, sym string, values map[string]string, idx uint16) (string, error) {
    150 	tpl, err := pg.resource.GetTemplate(ctx, sym)
    151 	if err != nil {
    152 		return "", err
    153 	}
    154 	tpl += pg.extra
    155 	if pg.err != nil {
    156 		derr := pg.Error()
    157 		Logg.DebugCtxf(ctx, "prepending error", "err", pg.err, "display", derr)
    158 		if len(tpl) == 0 {
    159 			tpl = derr
    160 		} else {
    161 			tpl = fmt.Sprintf("%s\n%s", derr, tpl)
    162 		}
    163 	}
    164 	if pg.sizer != nil {
    165 		values, err = pg.sizer.GetAt(values, idx)
    166 		if err != nil {
    167 			return "", err
    168 		}
    169 	} else if idx > 0 {
    170 		return "", fmt.Errorf("sizer needed for indexed render")
    171 	}
    172 	Logg.Debugf("render for", "index", idx)
    173 	
    174 	tp, err := template.New("tester").Option("missingkey=error").Parse(tpl)
    175 	if err != nil {
    176 		return "", err
    177 	}
    178 
    179 	b := bytes.NewBuffer([]byte{})
    180 	err = tp.Execute(b, values)
    181 	if err != nil {
    182 		return "", err
    183 	}
    184 	return b.String(), err
    185 }
    186 
    187 // Render renders the current mapped content and menu state against the template associated with the symbol.
    188 func(pg *Page) Render(ctx context.Context, sym string, idx uint16) (string, error) {
    189 	var err error
    190 
    191 	values, err := pg.prepare(ctx, sym, pg.cacheMap, idx)
    192 	if err != nil {
    193 		return "", err
    194 	}
    195 
    196 	return pg.render(ctx, sym, values, idx)
    197 }
    198 
    199 // Reset prepared the Page object for re-use.
    200 //
    201 // It clears mappings and removes the sink definition.
    202 func(pg *Page) Reset() {
    203 	pg.sink = nil
    204 	pg.extra = ""
    205 	pg.cacheMap = make(map[string]string)
    206 	if pg.menu != nil {
    207 		pg.menu.Reset()
    208 	}
    209 	if pg.sizer != nil {
    210 		pg.sizer.Reset()
    211 	}
    212 }
    213 
    214 // extract sink values to separate array, and set the content of sink in values map to zero-length string.
    215 //
    216 // this allows render of page with emptry content the sink symbol to discover remaining capacity.
    217 func(pg *Page) split(sym string, values map[string]string) (map[string]string, string, []string, error) {
    218 	var sink string
    219 	var sinkValues []string
    220 	noSinkValues := make(map[string]string)
    221 
    222 	for k, v := range values {
    223 		sz, err := pg.cache.ReservedSize(k)
    224 		if err != nil {
    225 			return nil, "", nil, err
    226 		}
    227 		if sz == 0 {
    228 			sink = k
    229 			sinkValues = strings.Split(v, "\n")
    230 			v = ""
    231 			Logg.Infof("found sink", "sym", sym, "sink", k)
    232 		}
    233 		noSinkValues[k] = v
    234 	}
    235 	
    236 	if sink == "" {
    237 		Logg.Tracef("no sink found", "sym", sym)
    238 		return values, "", nil, nil
    239 	}
    240 	return noSinkValues, sink, sinkValues, nil
    241 }
    242 
    243 // flatten the sink values array into a paged string.
    244 //
    245 // newlines (within the same page) render are defined by NUL (0x00).
    246 //
    247 // pages are separated by LF (0x0a).
    248 func(pg *Page) joinSink(sinkValues []string, remaining uint32, menuSizes [4]uint32) (string, uint16, error) {
    249 	l := 0
    250 	var count uint16
    251 	tb := strings.Builder{}
    252 	rb := strings.Builder{}
    253 
    254 	// remaining is remaining less one LF
    255 	netRemaining := remaining - 1
    256 
    257 	// BUG: this reserves the previous browse before we know we need it
    258 	if len(sinkValues) > 1 {
    259 		netRemaining -= menuSizes[1] - 1
    260 	}
    261 
    262 	for i, v := range sinkValues {
    263 		l += len(v)
    264 		Logg.Tracef("processing sink", "idx", i, "value", v, "netremaining", netRemaining, "l", l)
    265 		if uint32(l) > netRemaining - 1 {
    266 			if tb.Len() == 0 {
    267 				return "", 0, fmt.Errorf("capacity insufficient for sink field %v", i)
    268 			}
    269 			rb.WriteString(tb.String())
    270 			rb.WriteRune('\n')
    271 			c := uint32(rb.Len())
    272 			pg.sizer.AddCursor(c)
    273 			tb.Reset()
    274 			l = 0
    275 			if count == 0 {
    276 				netRemaining -= menuSizes[2]
    277 			}
    278 			count += 1
    279 		}
    280 		if tb.Len() > 0 {
    281 			tb.WriteByte(byte(0x00))
    282 			l += 1
    283 		}
    284 		tb.WriteString(v)
    285 	}
    286 
    287 	if tb.Len() > 0 {
    288 		rb.WriteString(tb.String())
    289 		count += 1
    290 	}
    291 
    292 	r := rb.String()
    293 	r = strings.TrimRight(r, "\n")
    294 	return r, count, nil
    295 }
    296 
    297 func(pg *Page) applyMenuSink(ctx context.Context) ([]string, error) {
    298 	s, err := pg.menu.WithDispose().WithPages().Render(ctx, 0)
    299 	if err != nil {
    300 		return nil, err
    301 	}
    302 	values := strings.Split(s, "\n")
    303 	return values, nil
    304 }
    305 
    306 // render menu and all syms except sink, split sink into display chunks
    307 func(pg *Page) prepare(ctx context.Context, sym string, values map[string]string, idx uint16) (map[string]string, error) {
    308 	if pg.sizer == nil {
    309 		return values, nil
    310 	}
    311 
    312 	// extract sink values
    313 	noSinkValues, sink, sinkValues, err := pg.split(sym, values)
    314 	if err != nil {
    315 		return nil, err
    316 	}
    317 
    318 	// check if menu is sink aswell, fail if it is.
    319 	if pg.menu != nil {
    320 		if pg.menu.IsSink() {
    321 			if sink != "" {
    322 				return values, fmt.Errorf("cannot use menu as sink when sink already mapped")
    323 			}
    324 			sinkValues, err = pg.applyMenuSink(ctx)
    325 			if err != nil {
    326 				return nil, err
    327 			}
    328 			sink = "_menu"
    329 			pg.extra = "\n{{._menu}}"
    330 			pg.sizer.sink = sink
    331 			noSinkValues[sink] = ""
    332 			Logg.DebugCtxf(ctx, "menu is sink", "items", len(sinkValues))
    333 		}
    334 	}
    335 
    336 	// pre-render template without sink
    337 	// this includes the menu before any browsing options have been added
    338 	pg.sizer.AddCursor(0)
    339 	s, err := pg.render(ctx, sym, noSinkValues, 0)
    340 	if err != nil {
    341 		return nil, err
    342 	}
    343 
    344 	// this is the available bytes left for sink content and browse menu
    345 	remaining, ok := pg.sizer.Check(s)
    346 	if !ok {
    347 		return nil, fmt.Errorf("capacity exceeded")
    348 	}
    349 
    350 	// pre-calculate the menu sizes for all browse conditions
    351 	var menuSizes [4]uint32
    352 	if pg.menu != nil {
    353 		menuSizes, err = pg.menu.Sizes(ctx)
    354 		if err != nil {
    355 			return nil, err
    356 		}
    357 	}
    358 	Logg.Debugf("calculated pre-navigation allocation", "bytes", remaining, "menusizes", menuSizes)
    359 
    360 	// process sink values array into newline-separated string
    361 	sinkString, count, err := pg.joinSink(sinkValues, remaining, menuSizes)
    362 	if err != nil {
    363 		return nil, err
    364 	}
    365 	noSinkValues[sink] = sinkString
    366 
    367 	// update the page count of the menu
    368 	if pg.menu != nil {
    369 		pg.menu = pg.menu.WithPageCount(count)
    370 	}
    371 
    372 	// write all sink values to log.
    373 	for i, v := range strings.Split(sinkString, "\n") {
    374 		Logg.Tracef("nosinkvalue", "idx", i, "value", v)
    375 	}
    376 
    377 	return noSinkValues, nil
    378 }
    379 
    380 // render template, menu (if it exists), and audit size constraint (if it exists).
    381 func(pg *Page) render(ctx context.Context, sym string, values map[string]string, idx uint16) (string, error) {
    382 	var ok bool
    383 	r := ""
    384 	s, err := pg.RenderTemplate(ctx, sym, values, idx)
    385 	if err != nil {
    386 		return "", err
    387 	}
    388 	Logg.Debugf("rendered template", "bytes", len(s))
    389 	r += s
    390 
    391 	if pg.menu != nil {
    392 		s, err = pg.menu.Render(ctx, idx)
    393 		if err != nil {
    394 			return "", err
    395 		}
    396 		l := len(s)
    397 		Logg.Debugf("rendered menu", "bytes", l)
    398 		if l > 0 {
    399 			r += "\n" + s
    400 		}
    401 	}
    402 
    403 	if pg.sizer != nil {
    404 		_, ok = pg.sizer.Check(r)
    405 		if !ok {
    406 			return "", fmt.Errorf("limit exceeded: %v", pg.sizer)
    407 		}
    408 	}
    409 	return r, nil
    410 }