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 }