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 }