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  // }