github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/domrender/renderer-js-instructions.go (about) 1 package domrender 2 3 import ( 4 "encoding/binary" 5 "errors" 6 "fmt" 7 "io" 8 "strings" 9 ) 10 11 // NOTE: I looked at using Protobuf for this, and in some ways it makes sense. The main issue though is that it brings in 12 // a bunch of code I don't necessarily need, particularly on the JS side. There are only a few data types that need to be 13 // encoded and it's not too big a deal. Whereas with protobuf I immediately bring in 250k of protobuf JS code, the vast 14 // majority of which is not needed. So I'm proceeding with the idea that the encoding/decoding is simple enough to just do 15 // by hand. I hope I'm right. -bgp 16 17 // NOTE: I needed a single concise word which means, essentially "make it so". The idea being that the element described 18 // should exist, and if it does not, update/replace whatever is there so it is. Unable to find a suitable term in the 19 // English language, I've chosen the word "Picard" for this purpose. UPDATE: Alas, this didn't pan out, but it was worth 20 // a try ;) 21 22 const ( 23 //nolint:golint,unused 24 opcodeEnd uint8 = 0 // no more instructions in this buffer 25 // opcodeClearRefmap uint8 = 1 // clear the reference map, all following instructions must not reference prior IDs 26 // opcodeClearElStack uint8 = 1 // clear the stack of elements 27 opcodeClearEl uint8 = 1 // unset current element 28 // opcodeSetHTMLRef uint8 = 2 // assign ref for html tag 29 // opcodeSetHeadRef uint8 = 3 // assign ref for head tag 30 // opcodeSetBodyRef uint8 = 4 // assign ref for body tag 31 // opcodeSelectRef uint8 = 5 // select element by ref 32 opcodeRemoveOtherAttrs uint8 = 5 // remove any elements for the current element that we didn't just set 33 opcodeSetAttrStr uint8 = 6 // assign attribute string to the current selected element 34 opcodeSelectMountPoint uint8 = 7 // selects the mount point element and pushes to the stack - the first time by selector but every subsequent time it will reuse the element from before (because the selector may not match after it's been synced over, it's id etc), also make sure it's of this element name and recreate if so 35 // opcodePicardFirstChildElement uint8 = 8 // ensure an element first child and select element 36 // opcodePicardFirstChildText uint8 = 9 // ensure a text first child and select element 37 // opcodePicardFirstChildComment uint8 = 10 // ensure a comment first child and select element 38 // opcodeSelectParent uint8 = 11 // select parent element 39 // opcodePicardFirstChild uint8 = 12 // ensure an element first child and select element 40 41 opcodeMoveToFirstChild uint8 = 20 // move node selection to first child (doesn't have to exist) 42 opcodeSetElement uint8 = 21 // assign current selected node as an element of the specified type 43 // opcodeSetElementAttr uint8 = 22 // set attribute on current element 44 opcodeSetText uint8 = 23 // assign current selected node as text with specified content 45 opcodeSetComment uint8 = 24 // assign current selected node as comment with specified content 46 opcodeMoveToParent uint8 = 25 // move node selection to parent 47 opcodeMoveToNextSibling uint8 = 26 // move node selection to next sibling (doesn't have to exist) 48 opcodeRemoveOtherEventListeners uint8 = 27 // remove all event listeners from currently selected element that were not just set 49 opcodeSetEventListener uint8 = 28 // assign event listener to currently selected element 50 opcodeSetInnerHTML uint8 = 29 // set the innerHTML for an element 51 52 opcodeSetCSSTag uint8 = 30 // write a CSS (style or link) tag 53 opcodeRemoveOtherCSSTags uint8 = 31 // remove any CSS tags that have not been written since the last call 54 opcodeSetJSTag uint8 = 32 // write a JS (script) tag 55 opcodeRemoveOtherJSTags uint8 = 33 // remove any JS tags that have not been written since the last call 56 57 opcodeSetProperty uint8 = 35 // assign a JS property to the current element 58 opcodeSelectQuery uint8 = 36 // select an element 59 opcodeBufferInnerHTML uint8 = 37 // pass chunked text to set as inner html, complete with opcodeSetInnerHTML 60 61 opcodeSetAttrNSStr uint8 = 38 // assign attribute string to the current selected namespaced element 62 opcodeSetElementNS uint8 = 39 // assign current selected node as an element of the specified type in the specified namespace 63 64 opcodeCallback uint8 = 40 // issue callback, sends just callbackID 65 opcodeCallbackLastElement uint8 = 41 // issue callback with callbackID and most recent element reference 66 67 ) 68 69 // newInstructionList will create a new instance backed by the specified slice and with a clearBufFunc 70 // that is called when the buffer is about to overflow. 71 func newInstructionList(buf []byte, flushBufFunc func(il *instructionList) error) *instructionList { 72 if buf == nil { 73 panic("buf is nil") 74 } 75 if flushBufFunc == nil { 76 panic("flushBufFunc is nil") 77 } 78 return &instructionList{ 79 buf: buf, 80 flushBufFunc: flushBufFunc, 81 } 82 } 83 84 type instructionList struct { 85 buf []byte 86 pos int 87 flushBufFunc func(il *instructionList) error 88 logWriter io.Writer // set to non-nil to enable debug log output 89 } 90 91 var errDoesNotFit = errors.New("requested instruction does not fit in the buffer") 92 93 func (il *instructionList) logf(f string, args ...interface{}) error { 94 if il.logWriter == nil { 95 return nil 96 } 97 if !strings.HasSuffix(f, "\n") { 98 f += "\n" 99 } 100 _, err := fmt.Fprintf(il.logWriter, "domrender ildebug: "+f, args...) 101 return err 102 } 103 104 func (il *instructionList) flush() error { 105 err := il.logf("flush() calling flushBufFunc") 106 if err != nil { 107 return err 108 } 109 110 err = il.flushBufFunc(il) 111 if err != nil { 112 return err 113 } 114 il.pos = 0 115 err = il.logf("flush() completed") 116 if err != nil { 117 return err 118 } 119 return nil 120 } 121 122 // checkLenAndFlush calls checkLen(), if it fails attempts to flush the buffer and checkLen again, at which point any error is returned. 123 func (il *instructionList) checkLenAndFlush(l int) error { 124 125 err := il.checkLen(l) 126 if err != nil { 127 128 if err == errDoesNotFit { 129 err = il.flush() 130 if err != nil { 131 return err 132 } 133 err = il.checkLen(l) 134 } 135 } 136 137 return err 138 } 139 140 func (il *instructionList) checkLen(l int) error { 141 if il.pos+l > len(il.buf)-1 { 142 return errDoesNotFit 143 } 144 return nil 145 } 146 147 //nolint:golint,unused 148 func (il *instructionList) writeEnd() error { 149 err := il.logf("writeEnd[%d]()", opcodeEnd) 150 if err != nil { 151 return err 152 } 153 il.buf[il.pos] = opcodeEnd 154 il.pos++ 155 return nil 156 } 157 158 func (il *instructionList) writeClearEl() error { 159 err := il.logf("writeClearEl[%d]()", opcodeClearEl) 160 if err != nil { 161 return err 162 } 163 164 err = il.checkLenAndFlush(1) 165 if err != nil { 166 return err 167 } 168 169 il.writeValUint8(opcodeClearEl) 170 171 return nil 172 } 173 174 func (il *instructionList) writeRemoveOtherAttrs() error { 175 err := il.logf("writeRemoveOtherAttrs[%d]()", opcodeRemoveOtherAttrs) 176 if err != nil { 177 return err 178 } 179 180 err = il.checkLenAndFlush(1) 181 if err != nil { 182 return err 183 } 184 185 il.writeValUint8(opcodeRemoveOtherAttrs) 186 187 return nil 188 } 189 190 func (il *instructionList) writeSetAttrStr(name, value string) error { 191 err := il.logf("writeSetAttrStr[%d](name=%q, value=%q)", opcodeSetAttrStr, name, value) 192 if err != nil { 193 return err 194 } 195 196 size := len(name) + len(value) + 9 197 198 err = il.checkLenAndFlush(size) 199 if err != nil { 200 return err 201 } 202 203 il.writeValUint8(opcodeSetAttrStr) 204 il.writeValString(name) 205 il.writeValString(value) 206 207 return nil 208 } 209 210 func (il *instructionList) writeSetAttrNSStr(namespace, name, value string) error { 211 err := il.logf("writeSetAttrNSStr[%d](ns=%q, name=%q, value=%q)", opcodeSetAttrNSStr, namespace, name, value) 212 if err != nil { 213 return err 214 } 215 216 size := len(namespace) + len(name) + len(value) + 13 217 err = il.checkLenAndFlush(size) 218 if err != nil { 219 return err 220 } 221 222 il.writeValUint8(opcodeSetAttrNSStr) 223 il.writeValString(namespace) 224 il.writeValString(name) 225 il.writeValString(value) 226 227 return nil 228 } 229 230 func (il *instructionList) writeSelectQuery(selector string) error { 231 err := il.logf("writeSelectQuery[%d](selector=%q)", opcodeSelectQuery, selector) 232 if err != nil { 233 return err 234 } 235 236 err = il.checkLenAndFlush(5 + len(selector)) 237 if err != nil { 238 return err 239 } 240 il.writeValUint8(opcodeSelectQuery) 241 il.writeValString(selector) 242 return nil 243 } 244 245 func (il *instructionList) writeSelectMountPoint(selector, nodeName string) error { 246 err := il.logf("writeSelectMountPoint[%d](selector=%q, nodeName=%q)", opcodeSelectMountPoint, selector, nodeName) 247 if err != nil { 248 return err 249 } 250 251 err = il.checkLenAndFlush(len(selector) + len(nodeName) + 9) 252 if err != nil { 253 return err 254 } 255 256 il.writeValUint8(opcodeSelectMountPoint) 257 il.writeValString(selector) 258 il.writeValString(nodeName) 259 260 return nil 261 262 } 263 264 func (il *instructionList) writeMoveToFirstChild() error { 265 err := il.logf("writeMoveToFirstChild[%d]()", opcodeMoveToFirstChild) 266 if err != nil { 267 return err 268 } 269 270 err = il.checkLenAndFlush(1) 271 if err != nil { 272 return err 273 } 274 275 il.writeValUint8(opcodeMoveToFirstChild) 276 277 return nil 278 } 279 280 func (il *instructionList) writeSetElement(nodeName string) error { 281 err := il.logf("writeSetElement[%d](nodeName=%q)", opcodeSetElement, nodeName) 282 if err != nil { 283 return err 284 } 285 286 err = il.checkLenAndFlush(len(nodeName) + 5) 287 if err != nil { 288 return err 289 } 290 291 il.writeValUint8(opcodeSetElement) 292 il.writeValString(nodeName) 293 294 return nil 295 296 } 297 298 func (il *instructionList) writeSetElementNS(nodeName, namespace string) error { 299 err := il.logf("writeSetElementNS[%d](nodeName=%q, ns=%q)", opcodeSetElementNS, nodeName, namespace) 300 if err != nil { 301 return err 302 } 303 304 size := len(nodeName) + len(namespace) + 9 305 err = il.checkLenAndFlush(size) 306 307 if err != nil { 308 return err 309 } 310 311 il.writeValUint8(opcodeSetElementNS) 312 il.writeValString(nodeName) 313 il.writeValString(namespace) 314 315 return nil 316 317 } 318 319 func (il *instructionList) writeSetText(text string) error { 320 err := il.logf("writeSetText[%d](text=%q)", opcodeSetText, text) 321 if err != nil { 322 return err 323 } 324 325 err = il.checkLenAndFlush(len(text) + 5) 326 if err != nil { 327 return err 328 } 329 330 il.writeValUint8(opcodeSetText) 331 il.writeValString(text) 332 333 return nil 334 335 } 336 337 func (il *instructionList) writeSetComment(comment string) error { 338 err := il.logf("writeSetComment[%d](comment=%q)", opcodeSetComment, comment) 339 if err != nil { 340 return err 341 } 342 343 err = il.checkLenAndFlush(len(comment) + 5) 344 if err != nil { 345 return err 346 } 347 348 il.writeValUint8(opcodeSetComment) 349 il.writeValString(comment) 350 351 return nil 352 353 } 354 355 func (il *instructionList) writeMoveToParent() error { 356 err := il.logf("writeMoveToParent[%d]()", opcodeMoveToParent) 357 if err != nil { 358 return err 359 } 360 361 err = il.checkLenAndFlush(1) 362 if err != nil { 363 return err 364 } 365 366 il.writeValUint8(opcodeMoveToParent) 367 368 return nil 369 } 370 371 func (il *instructionList) writeMoveToNextSibling() error { 372 err := il.logf("writeMoveToNextSibling[%d]()", opcodeMoveToNextSibling) 373 if err != nil { 374 return err 375 } 376 377 err = il.checkLenAndFlush(1) 378 if err != nil { 379 return err 380 } 381 382 il.writeValUint8(opcodeMoveToNextSibling) 383 384 return nil 385 } 386 387 func (il *instructionList) writeSetInnerHTML(html string) error { 388 err := il.logf("writeSetInnerHTML[%d](html=%q)", opcodeSetInnerHTML, html) 389 if err != nil { 390 return err 391 } 392 393 // Make sure there is room to write at least one byte 394 // (1 byte for opcode, 4 bytes for string length, 1 byte of data) 395 // [This further ensures that maxLen - il.pos > 0] 396 err = il.checkLenAndFlush(6) 397 if err != nil { 398 return err 399 } 400 401 remaining := html 402 maxLen := len(il.buf) - 6 403 for len(remaining) > maxLen-il.pos { 404 chunk := remaining[:maxLen-il.pos] 405 remaining = remaining[maxLen-il.pos:] 406 err := il.checkLenAndFlush(len(chunk) + 5) 407 if err != nil { 408 return err 409 } 410 411 il.writeValUint8(opcodeBufferInnerHTML) 412 il.writeValString(chunk) 413 il.flush() 414 } 415 416 err = il.checkLenAndFlush(len(remaining) + 5) 417 if err != nil { 418 return err 419 } 420 421 il.writeValUint8(opcodeSetInnerHTML) 422 il.writeValString(remaining) 423 424 return nil 425 } 426 427 func (il *instructionList) writeSetEventListener(positionID []byte, eventType string, capture, passive bool) error { 428 err := il.logf("writeSetInnerHTML[%d](positionID=%q, eventType=%q, capture=%v, passive=%v)", opcodeSetEventListener, positionID, eventType, capture, passive) 429 if err != nil { 430 return err 431 } 432 433 err = il.checkLenAndFlush(len(positionID) + len(eventType) + 11) 434 if err != nil { 435 return err 436 } 437 438 il.writeValUint8(opcodeSetEventListener) 439 il.writeValBytes(positionID) 440 il.writeValString(eventType) 441 442 captureB := uint8(0) 443 if capture { 444 captureB = 1 445 } 446 il.writeValUint8(captureB) 447 448 passiveB := uint8(0) 449 if passive { 450 passiveB = 1 451 } 452 il.writeValUint8(passiveB) 453 454 return nil 455 456 } 457 458 func (il *instructionList) writeRemoveOtherEventListeners(positionID []byte) error { 459 err := il.logf("writeRemoveOtherEventListeners[%d](positionID=%q)", opcodeRemoveOtherEventListeners, positionID) 460 if err != nil { 461 return err 462 } 463 464 err = il.checkLenAndFlush(5 + len(positionID)) 465 if err != nil { 466 return err 467 } 468 469 il.writeValUint8(opcodeRemoveOtherEventListeners) 470 il.writeValBytes(positionID) 471 472 return nil 473 474 } 475 476 func (il *instructionList) writeSetCSSTag(elementName string, textContent []byte, attrPairs []string) error { 477 err := il.logf("writeSetCSSTag[%d](elementName=%q, textContext=%q, attrPairs=%#v)", opcodeSetCSSTag, elementName, textContent, attrPairs) 478 if err != nil { 479 return err 480 } 481 482 if len(attrPairs) > 254 { 483 return fmt.Errorf("attrPairs is %d, too large, max is 254", len(attrPairs)) 484 } 485 486 var al = 0 487 for _, s := range attrPairs { 488 al += len(s) + 4 489 } 490 491 var l = 1 + // opcode 492 al + // attrs 493 // 8 + // hashCode 494 1 + // 1 byte for number of strings to read 495 len(elementName) + 4 + 496 len(textContent) + 4 497 498 err = il.checkLenAndFlush(l) 499 if err != nil { 500 return err 501 } 502 503 il.writeValUint8(opcodeSetCSSTag) 504 // il.writeValUint64(hashCode) 505 il.writeValString(elementName) 506 il.writeValBytes(textContent) 507 il.writeValUint8(uint8(len(attrPairs))) 508 for _, s := range attrPairs { 509 il.writeValString(s) 510 } 511 512 return nil 513 514 } 515 516 func (il *instructionList) writeRemoveOtherCSSTags() error { 517 err := il.logf("writeRemoveOtherCSSTags[%d]()", opcodeRemoveOtherCSSTags) 518 if err != nil { 519 return err 520 } 521 522 err = il.checkLenAndFlush(1) 523 if err != nil { 524 return err 525 } 526 527 il.writeValUint8(opcodeRemoveOtherCSSTags) 528 529 return nil 530 } 531 532 func (il *instructionList) writeSetProperty(key string, jsonValue []byte) error { 533 err := il.logf("writeSetProperty[%d](key=%q, jsonValue=%q)", opcodeSetProperty, key, jsonValue) 534 if err != nil { 535 return err 536 } 537 538 size := len(key) + len(jsonValue) + 9 539 err = il.checkLenAndFlush(size) 540 if err != nil { 541 return err 542 } 543 544 il.writeValUint8(opcodeSetProperty) 545 il.writeValString(key) 546 il.writeValBytes(jsonValue) 547 548 return nil 549 } 550 551 func (il *instructionList) writeCallback(callbackID uint32) error { 552 err := il.logf("writeCallback[%d](callbackID=%v)", opcodeCallback, callbackID) 553 if err != nil { 554 return err 555 } 556 557 size := 5 558 err = il.checkLenAndFlush(size) 559 if err != nil { 560 return err 561 } 562 563 il.writeValUint8(opcodeCallback) 564 il.writeValUint32(callbackID) 565 566 return nil 567 } 568 569 func (il *instructionList) writeCallbackLastElement(callbackID uint32) error { 570 err := il.logf("writeCallbackLastElement[%d](callbackID=%v)", opcodeCallbackLastElement, callbackID) 571 if err != nil { 572 return err 573 } 574 575 size := 5 576 err = il.checkLenAndFlush(size) 577 if err != nil { 578 return err 579 } 580 581 il.writeValUint8(opcodeCallbackLastElement) 582 il.writeValUint32(callbackID) 583 584 return nil 585 } 586 587 func (il *instructionList) writeValUint8(b uint8) { 588 il.buf[il.pos] = b 589 il.pos++ 590 } 591 592 func (il *instructionList) writeValUint32(v uint32) { 593 binary.BigEndian.PutUint32(il.buf[il.pos:il.pos+4], v) 594 il.pos += 4 595 } 596 597 //nolint:golint,unused 598 func (il *instructionList) writeValUint64(ref uint64) { 599 binary.BigEndian.PutUint64(il.buf[il.pos:il.pos+8], ref) 600 il.pos += 8 601 } 602 603 func (il *instructionList) writeValString(s string) { 604 605 lenstr := len(s) 606 pos := il.pos 607 608 // write length as uint32 609 binary.BigEndian.PutUint32(il.buf[pos:pos+4], uint32(lenstr)) 610 611 // copy bytes directly from string into buf 612 copy(il.buf[pos+4:pos+4+lenstr], s) 613 614 il.pos = pos + 4 + lenstr 615 } 616 617 func (il *instructionList) writeValBytes(s []byte) { 618 619 lenstr := len(s) 620 pos := il.pos 621 622 // write length as uint32 623 binary.BigEndian.PutUint32(il.buf[pos:pos+4], uint32(lenstr)) 624 625 // copy bytes directly from string into buf 626 copy(il.buf[pos+4:pos+4+lenstr], s) 627 628 il.pos = pos + 4 + lenstr 629 } 630 631 // // "element and text" pattern (used for script, style, link) goes like: 632 // // string - element name 633 // // string - text content (zero length means no text content) 634 // // uint32 - number of attributes 635 // // string... - string pairs of key and then value for attributes (number of pairs is number of attributes above, so 1 attr would be 1 in the uint32 above and 2 string - 2 would mean 4 strings, etc.) 636 // func (il *instructionList) writeValElementAndText(elName, textContent string, attrKV []string) error { 637 638 // return nil 639 // }