go-vise

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

page.go (10043B)


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