├── .gitignore ├── README.md ├── diff.go ├── doc.go ├── indexed_byte_reader.go ├── karma ├── bench-mac.conf.js ├── bench-windows.conf.js ├── go │ ├── bench.go │ └── test.go ├── js │ └── support │ │ └── polyfill │ │ └── typedarray.js ├── test-mac.conf.js └── test-windows.conf.js ├── parse.go ├── parse_bench_test.go ├── parse_test.go ├── patch.go ├── scripts ├── bench.sh └── test.sh └── tree.go /.gitignore: -------------------------------------------------------------------------------- 1 | karma/js/*.js 2 | karma/js/*.js.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vdom 2 | ==== 3 | 4 | [![GoDoc](https://godoc.org/github.com/albrow/vdom?status.svg)](https://godoc.org/github.com/albrow/vdom) 5 | 6 | vdom is a virtual dom implementation written in go which is compatible with 7 | [gopherjs](http://www.gopherjs.org/) and inspired by 8 | [react.js](http://facebook.github.io/react/). The primary purpose of 9 | vdom is to improve the performance of view rendering in 10 | [humble](https://github.com/soroushjp/humble), a framework that lets you write 11 | frontend web apps in pure go and compile them to js to be run in the browser. 12 | However, vdom is framework agnostic, and generally will work whenever you can 13 | render html for your views as a slice of bytes. 14 | 15 | 16 | Development Status 17 | ------------------ 18 | 19 | vdom is no longer actively maintained. Additionally, ad hoc testing suggests that it might currently 20 | be slower than `setInnerHTML` in at least some cases. 21 | 22 | Most users today will likely want to use WebAssembly instead of GopherJS. 23 | 24 | Still, this repo might be a decent starting point for anyone wishing to create a virtual DOM implementation 25 | in Go. 26 | 27 | Browser Compatibility 28 | --------------------- 29 | 30 | vdom has been tested and works with IE9+ and the latest versions of Chrome, Safari, and Firefox. 31 | 32 | Javascript code generated with gopherjs uses typed arrays, so in order to work with IE9, you will 33 | need a polyfill. There is one in karma/js/support/polyfill/typedarray.js which is used for the 34 | karma tests. 35 | 36 | 37 | Installing 38 | ---------- 39 | 40 | Assuming you have already installed go and set up your go workspace, you can install 41 | vdom like you would any other package: 42 | 43 | `go get github.com/albrow/vdom` 44 | 45 | Import in your go code: 46 | 47 | `import "github.com/albrow/vdom"` 48 | 49 | Install the latest version of [gopherjs](https://github.com/gopherjs/gopherjs), which 50 | compiles your go code to javascript: 51 | 52 | `go get -u github.com/gopherjs/gopherjs` 53 | 54 | When you are ready, compile your go code to javascript using the `gopherjs` command line 55 | tool. Then include the resulting js file in your application. 56 | 57 | 58 | Quickstart Guide 59 | ---------------- 60 | 61 | I'll update this section when all functionality is completed. For now, here's a preview 62 | of what usage will probably look like. 63 | 64 | Assuming you have a go html template called todo.tmpl: 65 | 66 | ```html 67 |
  • 68 | 69 | 70 | 71 | 72 |
  • 73 | ``` 74 | 75 | And a Todo view/model type that looks like this: 76 | 77 | ```go 78 | type Todo struct { 79 | Title string 80 | Completed bool 81 | Root dom.Element 82 | tree *vdom.Tree 83 | } 84 | ``` 85 | 86 | You could do the following: 87 | 88 | ```go 89 | var todoTmpl = template.Must(template.ParseFiles("todo.tmpl")) 90 | 91 | func (todo *Todo) Render() error { 92 | // Execute the template with the given todo and write to a buffer 93 | buf := bytes.NewBuffer([]byte{}) 94 | if err := tmpl.Execute(buf, todo); err != nil { 95 | return err 96 | } 97 | // Parse the resulting html into a virtual tree 98 | newTree, err := vdom.Parse(buf.Bytes()) 99 | if err != nil { 100 | return err 101 | } 102 | // Calculate the diff between this render and the last render 103 | patches, err := vdom.Diff(todo.tree, newTree) 104 | if err != nil { 105 | return err 106 | } 107 | // Effeciently apply changes to the actual DOM 108 | if err := patches.Patch(todo.Root); err != nil { 109 | return err 110 | } 111 | // Remember the virtual DOM state for the next render to diff against 112 | todo.tree = newTree 113 | } 114 | ``` 115 | 116 | Testing 117 | ------- 118 | 119 | vdom uses three sets of tests. If you're on a unix system, you can run all the tests 120 | in one go with `scripts/test.sh`. The script also compiles the go files to javsacript each 121 | time it runs. You will still need to install the dependencies for the script to work correctly. 122 | 123 | ### Go Tests 124 | 125 | Traditional go tests can be run with `go test .`. These tests are for code which does not 126 | interact with the DOM or depend on js-specific features. 127 | 128 | ### Gopherjs Tests 129 | 130 | You can run `gopherjs test github.com/albrow/vdom` to compile the same tests from above 131 | to javascript and tests them with node.js. This will also test some code which might depend 132 | on js-specific features (but not the DOM) and can't be tested with pure go. You will need 133 | to install [node.js](http://nodejs.org/) to run these tests. 134 | 135 | ### Karma Tests 136 | 137 | vdom uses karma and the jasmine test framework to test code that interacts with the DOM in 138 | real browsers. You will need to install these dependencies: 139 | 140 | - [node.js](http://nodejs.org/) 141 | - [karma](http://karma-runner.github.io/0.12/index.html) 142 | - [karma-jasmine](https://github.com/karma-runner/karma-jasmine) 143 | 144 | Don't forget to also install the karma command line tools with `npm install -g karma-cli`. 145 | 146 | You will also need to install a launcher for each browser you want to test with, as well as the 147 | browsers themselves. Typically you install a karma launcher with `npm install -g karma-chrome-launcher`. 148 | You can edit the config files `karma/test-mac.conf.js` and `karma/test-windows.conf.js` if you want 149 | to change the browsers that are tested on. The Mac OS config specifies Chrome, Firefox, and Safari, and 150 | the Windows config specifies IE9-11, Chrome, and Firefox. You only need to install IE11, since the 151 | older versions can be tested via emulation. 152 | 153 | Once you have installed all the dependencies, start karma with `karma start karma/test-mac.conf.js` or 154 | `karma start karma/test-windows.conf.js` depending on your operating system. If you are using a unix 155 | machine, simply copy one of the config files and edit the browsers section as needed. Once karma is 156 | running, you can keep it running in between tests. 157 | 158 | Next you need to compile the test.go file to javascript so it can run in the browsers: 159 | 160 | ``` 161 | gopherjs build karma/go/test.go -o karma/js/test.js 162 | ``` 163 | 164 | Finally run the tests: 165 | 166 | ``` 167 | karma run 168 | ``` 169 | 170 | Benchmarking 171 | ------------ 172 | 173 | vdom uses three sets of benchmarks. If you're on a unix system, you can run all the benchmarks 174 | in one go with `scripts/bench.sh`. The script also compiles the go files to javsacript each 175 | time it runs. You will still need to install the dependencies for the script to work correctly. 176 | 177 | **NOTE:** There are some additional dependencies for benchmarking that are not needed for testing. 178 | 179 | ### Go Benchmarks 180 | 181 | Traditional go benchmarks can be run with `go test -bench . -run none`. I don't expect you 182 | to be using vdom in a pure go context (but there's nothing stopping you from doing so!), so 183 | these tests mainly serve as a comparison to the gopherjs benchmarks. It also helps with 184 | catching obvious performance problems early. 185 | 186 | ### Gopherjs Benchmarks 187 | 188 | To compile the library to javascript and benchmark it with node.js, you can run 189 | `gopherjs test github.com/albrow/vdom --bench=. --run=none`. These benchmarks are only 190 | for code that doesn't interact directly with the DOM. You will need to install 191 | [node.js](http://nodejs.org/) to run these benchmarks. 192 | 193 | ### Karma Benchmarks 194 | 195 | vdom uses karma and benchmark.js to test code that interacts with the DOM in real browsers. 196 | You will need to install these dependencies: 197 | 198 | - [node.js](http://nodejs.org/) 199 | - [karma](http://karma-runner.github.io/0.12/index.html) 200 | - [karma-benchmark](https://github.com/JamieMason/karma-benchmark) 201 | 202 | Don't forget to also install the karma command line tools with `npm install -g karma-cli`. 203 | 204 | Just like with the tests, you will need to install a launcher for each browser you want to test with. 205 | 206 | Once you have installed all the dependencies, start karma with `karma start karma/bench-mac.conf.js` or 207 | `karma start karma/bench-windows.conf.js` depending on your operating system. We have to use 208 | different config files because of a [limitation of karma-benchmark](https://github.com/JamieMason/karma-benchmark/issues/7). 209 | You will probably want to kill karma and restart it if you were running it with the test configuration. 210 | If you are using a unix machine, simply copy one of the config files and edit the browsers section as 211 | needed. Once karma is running, you can keep it running in between benchmarks. 212 | 213 | Next you need to compile the bench.go file to javascript so it can run in the browsers: 214 | 215 | ``` 216 | gopherjs build karma/go/bench.go -o karma/js/bench.js 217 | ``` 218 | 219 | Finally run the benchmarks: 220 | 221 | ``` 222 | karma run 223 | ``` 224 | -------------------------------------------------------------------------------- /diff.go: -------------------------------------------------------------------------------- 1 | package vdom 2 | 3 | func Diff(t, other *Tree) (PatchSet, error) { 4 | patches := []Patcher{} 5 | if err := recursiveDiff(&patches, t.Children, other.Children); err != nil { 6 | return nil, err 7 | } 8 | return patches, nil 9 | } 10 | 11 | func recursiveDiff(patches *[]Patcher, nodes, otherNodes []Node) error { 12 | numOtherNodes := len(otherNodes) 13 | numNodes := len(nodes) 14 | minNumNodes := numOtherNodes 15 | if numOtherNodes > numNodes { 16 | // There are more otherNodes than there are nodes. 17 | // We should append the additional nodes. 18 | for _, otherNode := range otherNodes[numNodes:] { 19 | *patches = append(*patches, &Append{ 20 | Parent: otherNode.Parent(), 21 | Child: otherNode, 22 | }) 23 | } 24 | minNumNodes = numNodes 25 | } else if numNodes > numOtherNodes { 26 | // There are more nodes than there are otherNodes. 27 | // We should remove the additional children. 28 | for _, node := range nodes[numOtherNodes:] { 29 | *patches = append(*patches, &Remove{ 30 | Node: node, 31 | }) 32 | } 33 | minNumNodes = numOtherNodes 34 | } 35 | for i := 0; i < minNumNodes; i++ { 36 | otherNode := otherNodes[i] 37 | node := nodes[i] 38 | if match, _ := CompareNodes(node, otherNode, false); !match { 39 | // The nodes have different tag names or values. We should replace 40 | // node with otherNode 41 | *patches = append(*patches, &Replace{ 42 | Old: node, 43 | New: otherNode, 44 | }) 45 | continue 46 | } 47 | // NOTE: Since CompareNodes checks the type, 48 | // we can only reach here if the nodes are of 49 | // the same type. 50 | if otherEl, ok := otherNode.(*Element); ok { 51 | // Both nodes are elements. We need to treat them differently because 52 | // they have children and attributes. 53 | el := node.(*Element) 54 | // Add the patches needed to make the attributes match (if any) 55 | diffAttributes(patches, el, otherEl) 56 | // Recursively apply diff algorithm to each element's children 57 | recursiveDiff(patches, el.Children(), otherEl.Children()) 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | // diffAttributes compares the attributes in el to the attributes in otherEl 64 | // and adds the necessary patches to make the attributes in el match those in 65 | // otherEl 66 | func diffAttributes(patches *[]Patcher, el, otherEl *Element) { 67 | otherAttrs := otherEl.AttrMap() 68 | attrs := el.AttrMap() 69 | for attrName := range attrs { 70 | // Remove any attributes in el that are not found in otherEl 71 | if _, found := otherAttrs[attrName]; !found { 72 | *patches = append(*patches, &RemoveAttr{ 73 | Node: el, 74 | AttrName: attrName, 75 | }) 76 | } 77 | } 78 | // Now iterate through the attributes in otherEl 79 | for name, otherValue := range otherAttrs { 80 | value, found := attrs[name] 81 | if !found { 82 | // The attribute exists in otherEl but not in el, 83 | // we should add it. 84 | *patches = append(*patches, &SetAttr{ 85 | Node: el, 86 | Attr: &Attr{ 87 | Name: name, 88 | Value: otherValue, 89 | }, 90 | }) 91 | } else if value != otherValue { 92 | // The attribute exists in el but has a different value 93 | // than it does in otherEl. We should set it to the value 94 | // in otherEl. 95 | *patches = append(*patches, &SetAttr{ 96 | Node: el, 97 | Attr: &Attr{ 98 | Name: name, 99 | Value: otherValue, 100 | }, 101 | }) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // package vdom is a virtual dom implementation compatible with gopherjs 2 | package vdom 3 | -------------------------------------------------------------------------------- /indexed_byte_reader.go: -------------------------------------------------------------------------------- 1 | package vdom 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // NewIndexedByteReader returns a new IndexedByteReader which will 9 | // read from buf. 10 | func NewIndexedByteReader(buf []byte) *IndexedByteReader { 11 | return &IndexedByteReader{buf: buf} 12 | } 13 | 14 | // IndexedByteReader satisfies io.Reader and io.ByteReader and also 15 | // adds some additional methods for searching the buffer and returning 16 | // the current offset. 17 | type IndexedByteReader struct { 18 | buf []byte 19 | off int 20 | } 21 | 22 | // Read satisfies io.Reader. 23 | func (r *IndexedByteReader) Read(p []byte) (int, error) { 24 | n := copy(p, r.buf[r.off:]) 25 | r.off += n 26 | return n, nil 27 | } 28 | 29 | // ReadByte satisfies io.ByteReader. We expect that xml.Decoder will 30 | // upgrade the IndexedByteReader to a io.ByteReader and call this method 31 | // instead of Read. 32 | func (r *IndexedByteReader) ReadByte() (byte, error) { 33 | if r.off >= len(r.buf) { 34 | // Reached the end of the buffer 35 | return 0, io.EOF 36 | } 37 | c := r.buf[r.off] 38 | r.off++ 39 | return c, nil 40 | } 41 | 42 | // Offset returns the current offset position for r, i.e., 43 | // the number of bytes that have been read so far. 44 | func (r *IndexedByteReader) Offset() int { 45 | return r.off 46 | } 47 | 48 | // BackwardsSearch starts at start and iterates backwards through r.buf[min:max] 49 | // until it finds b. It returns the index of b if b was found within the given interval 50 | // and -1 if it was not. It returns an error if min or max is outside the bounds of r.buf. 51 | func (r *IndexedByteReader) BackwardsSearch(min int, max int, b byte) (int, error) { 52 | if min >= len(r.buf) || min < 0 { 53 | return -1, fmt.Errorf("Error in BackwardsSearch min %d is out of bounds. r has buf of length %d", min, len(r.buf)) 54 | } 55 | if max >= len(r.buf) || max < min { 56 | return -1, fmt.Errorf("Error in BackwardsSearch max %d is out of bounds. r has buf of length %d and min was %d", min, len(r.buf), min) 57 | } 58 | for j := max; j >= min; j-- { 59 | if r.buf[j] == b { 60 | return j, nil 61 | } 62 | } 63 | return -1, nil 64 | } 65 | -------------------------------------------------------------------------------- /karma/bench-mac.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration for benchmarking code that interactis with the DOM on 2 | // Mac OS X 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['benchmark'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'js/bench.js' 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [], 24 | 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 28 | preprocessors: {}, 29 | 30 | 31 | // test results reporter to use 32 | // possible values: 'dots', 'progress' 33 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 34 | reporters: ['benchmark'], 35 | 36 | 37 | // web server port 38 | port: 9876, 39 | 40 | 41 | // enable / disable colors in the output (reporters and logs) 42 | colors: true, 43 | 44 | 45 | // level of logging 46 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 47 | logLevel: config.LOG_INFO, 48 | 49 | 50 | // enable / disable watching file and executing tests whenever any file changes 51 | autoWatch: false, 52 | 53 | 54 | // start these browsers 55 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 56 | browsers: [ 57 | 'Chrome-Bench', 58 | 'Firefox', 59 | 'Safari' 60 | ], 61 | 62 | customLaunchers: { 63 | 'Chrome-Bench': { 64 | base: 'Chrome', 65 | flags: ['--enable-benchmarking'] 66 | } 67 | }, 68 | 69 | 70 | // Continuous Integration mode 71 | // if true, Karma captures browsers, runs the tests and exits 72 | singleRun: false 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /karma/bench-windows.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration for benchmarking code that interactis with the DOM on 2 | // Windows Operating Systems 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['benchmark'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'js/support/polyfill/*.js', // polyfill for older versions of IE 19 | 'js/bench.js' 20 | ], 21 | 22 | 23 | // list of files to exclude 24 | exclude: [], 25 | 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: {}, 30 | 31 | 32 | // test results reporter to use 33 | // possible values: 'dots', 'progress' 34 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 35 | reporters: ['benchmark'], 36 | 37 | 38 | // web server port 39 | port: 9876, 40 | 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | 46 | // level of logging 47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | 51 | // enable / disable watching file and executing tests whenever any file changes 52 | autoWatch: false, 53 | 54 | 55 | // start these browsers 56 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 57 | browsers: [ 58 | 'Chrome', 59 | 'Firefox', 60 | 'IE', 61 | 'IE10', 62 | 'IE9' 63 | ], 64 | 65 | customLaunchers: { 66 | IE10: { 67 | base: 'IE', 68 | 'x-ua-compatible': 'IE=EmulateIE10' 69 | }, 70 | IE9: { 71 | base: 'IE', 72 | 'x-ua-compatible': 'IE=EmulateIE9' 73 | } 74 | }, 75 | 76 | 77 | // Continuous Integration mode 78 | // if true, Karma captures browsers, runs the tests and exits 79 | singleRun: false 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /karma/go/bench.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/albrow/vdom" 6 | "github.com/gopherjs/gopherjs/js" 7 | "honnef.co/go/js/dom" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | document = dom.GetWindow().Document() 14 | sandbox dom.Element 15 | ) 16 | 17 | func init() { 18 | sandbox = document.CreateElement("div") 19 | sandbox.SetID("sandbox") 20 | document.QuerySelector("body").AppendChild(sandbox) 21 | } 22 | 23 | func main() { 24 | benchmarkRenderList(sandbox, 3, 1) 25 | benchmarkRenderList(sandbox, 10, 1) 26 | benchmarkRenderList(sandbox, 10, 5) 27 | benchmarkRenderList(sandbox, 10, 10) 28 | benchmarkRenderList(sandbox, 100, 1) 29 | benchmarkRenderList(sandbox, 100, 50) 30 | benchmarkRenderList(sandbox, 100, 100) 31 | } 32 | 33 | // generateList returns html for an unordered list of n items. 34 | func generateList(n int) []byte { 35 | result := []byte("")...) 42 | return result 43 | } 44 | 45 | // changeList accepts html that contains an unordered list of n 46 | // items and changes the first n items. It returns the new html 47 | // with the changes applied. 48 | func changeList(old []byte, n int) []byte { 49 | newHTML := string(old) 50 | for i := 0; i < n && i < len(old); i++ { 51 | oldItem := fmt.Sprintf("
  • %d
  • ", i) 52 | newItem := fmt.Sprintf("
  • new %d
  • ", i) 53 | newHTML = strings.Replace(newHTML, oldItem, newItem, 1) 54 | } 55 | return []byte(newHTML) 56 | } 57 | 58 | // benchmarkRenderList generates html for an unordered list of numItems 59 | // items and sets it as the inner html of root. Then it re-renders the list, 60 | // making numChanges changes. It compares re-rendering with the virtual DOM 61 | // vs. re-rendering via setHTML. 62 | func benchmarkRenderList(root dom.Element, numItems, numChanges int) { 63 | oldHTML := generateList(numItems) 64 | newHTML := changeList(oldHTML, numChanges) 65 | oldTree, err := vdom.Parse(oldHTML) 66 | if err != nil { 67 | panic(err) 68 | } 69 | suiteName := fmt.Sprintf("Re-render list with %d items after %d changes", numItems, numChanges) 70 | js.Global.Call("suite", suiteName, func() { 71 | 72 | js.Global.Call("benchmark", "with SetInnerHTML", func() { 73 | root.SetInnerHTML(string(newHTML)) 74 | }) 75 | 76 | js.Global.Call("benchmark", "with virtual DOM", func() { 77 | newTree, err := vdom.Parse(newHTML) 78 | if err != nil { 79 | panic(err) 80 | } 81 | patches, err := vdom.Diff(oldTree, newTree) 82 | if err != nil { 83 | panic(err) 84 | } 85 | if err := patches.Patch(root); err != nil { 86 | panic(err) 87 | } 88 | }) 89 | }, js.MakeWrapper(map[string]interface{}{ 90 | "setup": func() { 91 | root.SetInnerHTML(string(oldHTML)) 92 | }, 93 | })) 94 | } 95 | -------------------------------------------------------------------------------- /karma/go/test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/JohannWeging/jasmine" 5 | "github.com/albrow/vdom" 6 | "honnef.co/go/js/dom" 7 | ) 8 | 9 | var ( 10 | document = dom.GetWindow().Document() 11 | ) 12 | 13 | func main() { 14 | 15 | // The body element will be used throughout all tests, often as the 16 | // root or starting point of the virtual tree in the actual DOM. 17 | var body dom.Element 18 | 19 | // Before each test, instantiate the body variable if it is not 20 | // already instantiated. 21 | jasmine.BeforeEach(func() { 22 | if body == nil { 23 | body = document.QuerySelector("body") 24 | } 25 | }) 26 | 27 | // After each test, remove everything inside the body element in order 28 | // to prepare for the next test. 29 | jasmine.AfterEach(func() { 30 | body.SetInnerHTML("") 31 | }) 32 | 33 | // This test is just checks that the code cross-compiled correctly and can 34 | // be executed by the karma test runner. 35 | jasmine.Describe("Tests", func() { 36 | jasmine.It("can be loaded", func() { 37 | jasmine.Expect(true).ToBe(true) 38 | }) 39 | }) 40 | 41 | // Test the Element.Selector method in the actual DOM with various different 42 | // html structures. 43 | jasmine.Describe("Selector", func() { 44 | 45 | jasmine.It("works with a single root element", func() { 46 | // Parse some source html into a tree 47 | html := "
    " 48 | tree := setUpDOM(html, body) 49 | testSelectors(tree, body) 50 | }) 51 | 52 | jasmine.It("works with a ul and nested lis", func() { 53 | // Parse some html into a tree 54 | html := "" 55 | tree := setUpDOM(html, body) 56 | testSelectors(tree, body) 57 | }) 58 | 59 | jasmine.It("works with a form with autoclosed tags", func() { 60 | // Parse some html into a tree 61 | html := `
    ` 62 | tree := setUpDOM(html, body) 63 | testSelectors(tree, body) 64 | }) 65 | }) 66 | 67 | // Test the Append Patcher in the actual DOM with various different html 68 | // structures. 69 | jasmine.Describe("Append", func() { 70 | 71 | jasmine.It("works with a single root element", func() { 72 | testAppendRootPatcher(body, "
    ") 73 | }) 74 | 75 | jasmine.It("works with a text root", func() { 76 | testAppendRootPatcher(body, "Text") 77 | }) 78 | 79 | jasmine.It("works with a comment root", func() { 80 | testAppendRootPatcher(body, "") 81 | }) 82 | 83 | jasmine.It("works with nested siblings", func() { 84 | createAndApplyPatcher(body, "", func(tree *vdom.Tree) vdom.Patcher { 85 | // Create a new tree, which only consists of a new li element 86 | // which we want to append 87 | newTree, err := vdom.Parse([]byte("
  • three
  • ")) 88 | jasmine.Expect(err).ToBe(nil) 89 | // Create a patch manually 90 | return &vdom.Append{ 91 | Child: newTree.Children[0], 92 | Parent: tree.Children[0].(*vdom.Element), 93 | } 94 | }) 95 | // Test that the patch was applied 96 | ul := body.ChildNodes()[0].(*dom.HTMLUListElement) 97 | jasmine.Expect(ul.InnerHTML()).ToBe("
  • one
  • two
  • three
  • ") 98 | }) 99 | }) 100 | 101 | // Test the Replace Patcher in the actual DOM with various different html 102 | // structures. 103 | jasmine.Describe("Replace", func() { 104 | 105 | jasmine.It("works with a single root element", func() { 106 | testReplaceRootPatcher(body, `
    `, `
    `) 107 | }) 108 | 109 | jasmine.It("works with a text root", func() { 110 | testReplaceRootPatcher(body, "Old", "New") 111 | }) 112 | 113 | jasmine.It("works with a comment root", func() { 114 | testReplaceRootPatcher(body, "", "") 115 | }) 116 | 117 | jasmine.It("works with nested siblings", func() { 118 | createAndApplyPatcher(body, "", func(tree *vdom.Tree) vdom.Patcher { 119 | // Create a new tree, which only consists of one of the lis 120 | // We want to change it from one to uno 121 | newTree, err := vdom.Parse([]byte("
  • uno
  • ")) 122 | jasmine.Expect(err).ToBe(nil) 123 | // Create a patch manually 124 | return &vdom.Replace{ 125 | Old: tree.Children[0].Children()[0], 126 | New: newTree.Children[0], 127 | } 128 | }) 129 | // Test that the patch was applied 130 | ul := body.ChildNodes()[0].(*dom.HTMLUListElement) 131 | jasmine.Expect(ul.InnerHTML()).ToBe("
  • uno
  • two
  • three
  • ") 132 | }) 133 | }) 134 | 135 | // Test the Remove Patcher in the actual DOM with various different html 136 | // structures. 137 | jasmine.Describe("Remove", func() { 138 | 139 | jasmine.It("works with a single root element", func() { 140 | testRemoveRootPatcher(body, "
    ") 141 | }) 142 | 143 | jasmine.It("works with a text root", func() { 144 | testRemoveRootPatcher(body, "Text") 145 | }) 146 | 147 | jasmine.It("works with a comment root", func() { 148 | testRemoveRootPatcher(body, "") 149 | }) 150 | 151 | jasmine.It("works with nested siblings", func() { 152 | createAndApplyPatcher(body, "", func(tree *vdom.Tree) vdom.Patcher { 153 | return &vdom.Remove{ 154 | Node: tree.Children[0].Children()[1], 155 | } 156 | }) 157 | // Test that the patch was applied by checking the innerHTML 158 | // property of the ul node. 159 | ul := body.ChildNodes()[0].(*dom.HTMLUListElement) 160 | jasmine.Expect(ul.InnerHTML()).ToBe("
  • one
  • three
  • ") 161 | }) 162 | }) 163 | 164 | // Test the SetAttr Patcher in the actual DOM with various different html 165 | // structures. 166 | jasmine.Describe("SetAttr", func() { 167 | 168 | jasmine.It("works on a root element", func() { 169 | createAndApplyPatcher(body, "
    ", func(tree *vdom.Tree) vdom.Patcher { 170 | return &vdom.SetAttr{ 171 | Node: tree.Children[0], 172 | Attr: &vdom.Attr{ 173 | Name: "id", 174 | Value: "foo", 175 | }, 176 | } 177 | }) 178 | // Test that the patch was applied 179 | jasmine.Expect(body.InnerHTML()).ToBe(`
    `) 180 | }) 181 | 182 | jasmine.It("works on a nested element", func() { 183 | createAndApplyPatcher(body, "", func(tree *vdom.Tree) vdom.Patcher { 184 | return &vdom.SetAttr{ 185 | Node: tree.Children[0].Children()[1], 186 | Attr: &vdom.Attr{ 187 | Name: "data-value", 188 | Value: "two", 189 | }, 190 | } 191 | }) 192 | // Test that the patch was applied 193 | ul := body.ChildNodes()[0].(*dom.HTMLUListElement) 194 | jasmine.Expect(ul.InnerHTML()).ToBe(`
  • one
  • two
  • three
  • `) 195 | }) 196 | }) 197 | 198 | // Test the RemoveAttr Patcher in the actual DOM with various different html 199 | // structures. 200 | jasmine.Describe("RemoveAttr", func() { 201 | 202 | jasmine.It("works on a root element", func() { 203 | createAndApplyPatcher(body, `
    `, func(tree *vdom.Tree) vdom.Patcher { 204 | return &vdom.RemoveAttr{ 205 | Node: tree.Children[0], 206 | AttrName: "id", 207 | } 208 | }) 209 | // Test that the patch was applied 210 | jasmine.Expect(body.InnerHTML()).ToBe("
    ") 211 | }) 212 | 213 | jasmine.It("works on a nested element", func() { 214 | createAndApplyPatcher(body, ``, func(tree *vdom.Tree) vdom.Patcher { 215 | return &vdom.RemoveAttr{ 216 | Node: tree.Children[0].Children()[1], 217 | AttrName: "data-value", 218 | } 219 | }) 220 | // Test that the patch was applied 221 | ul := body.ChildNodes()[0].(*dom.HTMLUListElement) 222 | jasmine.Expect(ul.InnerHTML()).ToBe("
  • one
  • two
  • three
  • ") 223 | }) 224 | 225 | }) 226 | 227 | // Test the Diff function in the actual DOM with various different html 228 | // structures. 229 | jasmine.Describe("Diff", func() { 230 | 231 | jasmine.It("creates a root element", func() { 232 | testDiff(body, "", "
    ") 233 | }) 234 | 235 | jasmine.It("removes a root element", func() { 236 | testDiff(body, "
    ", "") 237 | }) 238 | 239 | jasmine.It("replaces a root element", func() { 240 | testDiff(body, "
    ", "") 241 | }) 242 | 243 | jasmine.It("creates a root text node", func() { 244 | testDiff(body, "", "Text") 245 | }) 246 | 247 | jasmine.It("removes a root text node", func() { 248 | testDiff(body, "Text", "") 249 | }) 250 | 251 | jasmine.It("replaces a root text node", func() { 252 | testDiff(body, "OldText", "NewText") 253 | }) 254 | 255 | jasmine.It("creates a root comment node", func() { 256 | testDiff(body, "", "") 257 | }) 258 | 259 | jasmine.It("removes a root comment node", func() { 260 | testDiff(body, "", "") 261 | }) 262 | 263 | jasmine.It("replaces a root comment node", func() { 264 | testDiff(body, "", "") 265 | }) 266 | 267 | jasmine.It("adds a root element attribute", func() { 268 | testDiff(body, "
    ", `
    `) 269 | }) 270 | 271 | jasmine.It("removes a root element attribute", func() { 272 | testDiff(body, `
    `, "
    ") 273 | }) 274 | 275 | jasmine.It("replaces a root element attribute", func() { 276 | testDiff(body, `
    `, `
    `) 277 | }) 278 | 279 | jasmine.It("creates a nested element", func() { 280 | testDiff(body, "
    ", "
    ") 281 | }) 282 | 283 | jasmine.It("removes a nested element", func() { 284 | testDiff(body, "
    ", "
    ") 285 | }) 286 | 287 | jasmine.It("replaces a nested element", func() { 288 | testDiff(body, "
    ", "
    ") 289 | }) 290 | 291 | jasmine.It("creates a nested text node", func() { 292 | testDiff(body, "
    ", "
    Text
    ") 293 | }) 294 | 295 | jasmine.It("removes a nested text node", func() { 296 | testDiff(body, "
    Text
    ", "
    ") 297 | }) 298 | 299 | jasmine.It("replaces a nested text node", func() { 300 | testDiff(body, "
    OldText
    ", "
    NewText
    ") 301 | }) 302 | 303 | jasmine.It("creates a nested comment node", func() { 304 | testDiff(body, "
    ", "
    ") 305 | }) 306 | 307 | jasmine.It("removes a nested comment node", func() { 308 | testDiff(body, "
    ", "
    ") 309 | }) 310 | 311 | jasmine.It("replaces a nested comment node", func() { 312 | testDiff(body, "
    ", "
    ") 313 | }) 314 | 315 | jasmine.It("adds a nested element attribute", func() { 316 | testDiff(body, "
    ", `
    `) 317 | }) 318 | 319 | jasmine.It("removes a nested element attribute", func() { 320 | testDiff(body, `
    `, "
    ") 321 | }) 322 | 323 | jasmine.It("replaces a nested element attribute", func() { 324 | testDiff(body, `
    `, `
    `) 325 | }) 326 | 327 | jasmine.It("creates a nested element with siblings", func() { 328 | testDiff(body, "", "") 329 | }) 330 | 331 | jasmine.It("removes a nested element with siblings", func() { 332 | testDiff(body, "", "") 333 | }) 334 | 335 | jasmine.It("replaces a nested element siblings", func() { 336 | testDiff(body, "", "") 337 | }) 338 | 339 | jasmine.It("adds/replaces multiple attributes", func() { 340 | // Since the order of attributes can change, we'll have to do this test 341 | // manually 342 | oldHTML := `
    ` 343 | newHTML := `
    ` 344 | // Parse some source oldHTML into a tree and add it 345 | // to the actual DOM 346 | tree := setUpDOM(oldHTML, body) 347 | // Create a virtual tree with the newHTML 348 | newTree, err := vdom.Parse([]byte(newHTML)) 349 | jasmine.Expect(err).ToBe(nil) 350 | // Use the diff function to calculate the difference between 351 | // the trees and return a patch set 352 | patches, err := vdom.Diff(tree, newTree) 353 | jasmine.Expect(err).ToBe(nil) 354 | // Apply the patches to the body in the actual DOM 355 | err = patches.Patch(body) 356 | jasmine.Expect(err).ToBe(nil) 357 | // Check that the body now has innerHTML equal to newHTML, 358 | // which would indecate the diff and patch set worked as 359 | // expected 360 | jasmine.Expect(len(body.ChildNodes())).ToBe(1) 361 | div := body.ChildNodes()[0].(dom.Element) 362 | expectedAttributes := map[string]string{ 363 | "class": "bar", 364 | "id": "foo", 365 | "name": "biz", 366 | "onClick": "doStuff()", 367 | } 368 | jasmine.Expect(div.Underlying().Get("attributes").Get("length")).ToBe(len(expectedAttributes)) 369 | for name, value := range expectedAttributes { 370 | jasmine.Expect(div.HasAttribute(name)).ToBe(true) 371 | jasmine.Expect(div.GetAttribute(name)).ToBe(value) 372 | } 373 | }) 374 | 375 | }) 376 | } 377 | 378 | // setUpDOM parses html into a virtual tree, then adds it to the 379 | // actual dom by appending to body. It returns both the virtual 380 | // tree. 381 | func setUpDOM(html string, body dom.Element) *vdom.Tree { 382 | // Parse the html into a virtual tree 383 | vtree, err := vdom.Parse([]byte(html)) 384 | jasmine.Expect(err).ToBe(nil) 385 | // Add html to the actual DOM 386 | body.SetInnerHTML(html) 387 | return vtree 388 | } 389 | 390 | // expectExistsInDOM invokes jasmine and the dom bindings to check that 391 | // el exists in the DOM. If it does not, jasmine will report an error. 392 | func expectExistsInDOM(root dom.Element, el dom.Element) { 393 | jasmine.Expect(root.Contains(el)).ToBe(true) 394 | } 395 | 396 | // testSelector tests the Selector method for vEl and then recursively 397 | // iterates through its children and tests the Selector method for them 398 | // as well. 399 | func testSelector(vEl *vdom.Element, root, expectedEl dom.Element) { 400 | gotEl := root.QuerySelector(vEl.Selector()) 401 | expectExistsInDOM(root, gotEl) 402 | jasmine.Expect(gotEl).ToEqual(expectedEl) 403 | // Test vEl's children recursively 404 | for i, vChild := range vEl.Children() { 405 | if vChildEl, ok := vChild.(*vdom.Element); ok { 406 | // If vRoot is an element, test its Selector method 407 | expectedChildEl := expectedEl.ChildNodes()[i].(dom.Element) 408 | testSelector(vChildEl, root, expectedChildEl) 409 | } 410 | } 411 | } 412 | 413 | // testSelectors recursively iterates through the virtual tree and the 414 | // corresponding nodes in the actual DOM and tests the Selector method 415 | // for every element. 416 | func testSelectors(tree *vdom.Tree, root dom.Element) { 417 | for i, vRoot := range tree.Children { 418 | if vEl, ok := vRoot.(*vdom.Element); ok { 419 | // If vRoot is an element, test its Selector method 420 | expectedEl := root.ChildNodes()[i].(dom.Element) 421 | testSelector(vEl, root, expectedEl) 422 | } 423 | } 424 | } 425 | 426 | // createAndApplyPatcher adds the given html to the actual DOM starting at 427 | // the given root. Then it invokes the createPatch function to create a Patcher 428 | // that will act on the new nodes that were created. 429 | func createAndApplyPatcher(root dom.Element, html string, createPatch func(tree *vdom.Tree) vdom.Patcher) { 430 | // Parse some source html into a tree 431 | tree := setUpDOM(html, root) 432 | // Create the patch using the provided function 433 | patch := createPatch(tree) 434 | // Apply the patch using the provided root 435 | err := patch.Patch(root) 436 | jasmine.Expect(err).ToBe(nil) 437 | } 438 | 439 | // newAppendRootPatcher returns an Append Patcher which will simply append a 440 | // new node with the given newHTML to the DOM at the root of the tree. 441 | func newAppendRootPatcher(newHTML string) func(tree *vdom.Tree) vdom.Patcher { 442 | return func(tree *vdom.Tree) vdom.Patcher { 443 | // Create a new tree with the given html 444 | newTree, err := vdom.Parse([]byte(newHTML)) 445 | jasmine.Expect(err).ToBe(nil) 446 | // Return a new patch to append to the root 447 | return &vdom.Append{ 448 | Child: newTree.Children[0], 449 | } 450 | } 451 | } 452 | 453 | // testAppendRootPatcher will set up the DOM with an empty root, then create 454 | // and apply a patch that should append a new element directly to the root, and 455 | // finally it tests that the patch was applied correctly. 456 | func testAppendRootPatcher(root dom.Element, newHTML string) { 457 | createAndApplyPatcher(root, "", newAppendRootPatcher(newHTML)) 458 | jasmine.Expect(root.InnerHTML()).ToBe(newHTML) 459 | } 460 | 461 | // newReplaceRootPatcher returns an Replace Patcher which will simply replace 462 | // the contents of the root with a new element created with newHTML. 463 | func newReplaceRootPatcher(newHTML string) func(tree *vdom.Tree) vdom.Patcher { 464 | return func(tree *vdom.Tree) vdom.Patcher { 465 | // Create a new tree with the given html 466 | newTree, err := vdom.Parse([]byte(newHTML)) 467 | jasmine.Expect(err).ToBe(nil) 468 | // Return a new patch to replace the root of the old tree with 469 | // the root of the new tree 470 | return &vdom.Replace{ 471 | Old: tree.Children[0], 472 | New: newTree.Children[0], 473 | } 474 | } 475 | } 476 | 477 | // testReplaceRootPatcher will set up the DOM with a root element created from the 478 | // given oldHTML, then it will create and apply a patch that should replace the content 479 | // of the root with a new element created from newHTML, and finally it tests that the 480 | // patch was applied correctly. 481 | func testReplaceRootPatcher(root dom.Element, oldHTML string, newHTML string) { 482 | createAndApplyPatcher(root, oldHTML, newReplaceRootPatcher(newHTML)) 483 | // Test that the patch was applied 484 | children := root.ChildNodes() 485 | jasmine.Expect(len(children)).ToBe(1) 486 | jasmine.Expect(root.InnerHTML()).ToBe(newHTML) 487 | } 488 | 489 | // newRemoveRootPatcher returns an Remove Patcher which will simply remove 490 | // the first child of the root. 491 | func newRemoveRootPatcher() func(tree *vdom.Tree) vdom.Patcher { 492 | return func(tree *vdom.Tree) vdom.Patcher { 493 | // Return a new patch to remove the root from the tree 494 | return &vdom.Remove{ 495 | Node: tree.Children[0], 496 | } 497 | } 498 | } 499 | 500 | // testReplaceRootPatcher will set up the DOM with a root element created from the 501 | // given html, then it will create and apply a patch that should remove the first child 502 | // of the root, and finally it tests that the patch was applied correctly. 503 | func testRemoveRootPatcher(root dom.Element, html string) { 504 | createAndApplyPatcher(root, html, newRemoveRootPatcher()) 505 | // Test that the patch was applied by testing that the 506 | // root has no children 507 | children := root.ChildNodes() 508 | jasmine.Expect(len(children)).ToBe(0) 509 | } 510 | 511 | func testDiff(root dom.Element, oldHTML string, newHTML string) { 512 | // Parse some source oldHTML into a tree and add it 513 | // to the actual DOM 514 | tree := setUpDOM(oldHTML, root) 515 | // Create a virtual tree with the newHTML 516 | newTree, err := vdom.Parse([]byte(newHTML)) 517 | jasmine.Expect(err).ToBe(nil) 518 | // Use the diff function to calculate the difference between 519 | // the trees and return a patch set 520 | patches, err := vdom.Diff(tree, newTree) 521 | jasmine.Expect(err).ToBe(nil) 522 | // Apply the patches to the root in the actual DOM 523 | err = patches.Patch(root) 524 | jasmine.Expect(err).ToBe(nil) 525 | // Check that the root now has innerHTML equal to newHTML, 526 | // which would indecate the diff and patch set worked as 527 | // expected 528 | jasmine.Expect(root.InnerHTML()).ToBe(newHTML) 529 | } 530 | -------------------------------------------------------------------------------- /karma/js/support/polyfill/typedarray.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010, Linden Research, Inc. 3 | Copyright (c) 2014, Joshua Bell 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 18 | THE SOFTWARE. 19 | $/LicenseInfo$ 20 | */ 21 | 22 | // Original can be found at: 23 | // https://bitbucket.org/lindenlab/llsd 24 | // Modifications by Joshua Bell inexorabletash@gmail.com 25 | // https://github.com/inexorabletash/polyfill 26 | 27 | // ES3/ES5 implementation of the Krhonos Typed Array Specification 28 | // Ref: http://www.khronos.org/registry/typedarray/specs/latest/ 29 | // Date: 2011-02-01 30 | // 31 | // Variations: 32 | // * Allows typed_array.get/set() as alias for subscripts (typed_array[]) 33 | // * Gradually migrating structure from Khronos spec to ES6 spec 34 | (function(global) { 35 | 'use strict'; 36 | var undefined = (void 0); // Paranoia 37 | 38 | // Beyond this value, index getters/setters (i.e. array[0], array[1]) are so slow to 39 | // create, and consume so much memory, that the browser appears frozen. 40 | var MAX_ARRAY_LENGTH = 1e5; 41 | 42 | // Approximations of internal ECMAScript conversion functions 43 | function Type(v) { 44 | switch(typeof v) { 45 | case 'undefined': return 'undefined'; 46 | case 'boolean': return 'boolean'; 47 | case 'number': return 'number'; 48 | case 'string': return 'string'; 49 | default: return v === null ? 'null' : 'object'; 50 | } 51 | } 52 | 53 | // Class returns internal [[Class]] property, used to avoid cross-frame instanceof issues: 54 | function Class(v) { return Object.prototype.toString.call(v).replace(/^\[object *|\]$/g, ''); } 55 | function IsCallable(o) { return typeof o === 'function'; } 56 | function ToObject(v) { 57 | if (v === null || v === undefined) throw TypeError(); 58 | return Object(v); 59 | } 60 | function ToInt32(v) { return v >> 0; } 61 | function ToUint32(v) { return v >>> 0; } 62 | 63 | // Snapshot intrinsics 64 | var LN2 = Math.LN2, 65 | abs = Math.abs, 66 | floor = Math.floor, 67 | log = Math.log, 68 | max = Math.max, 69 | min = Math.min, 70 | pow = Math.pow, 71 | round = Math.round; 72 | 73 | // emulate ES5 getter/setter API using legacy APIs 74 | // http://blogs.msdn.com/b/ie/archive/2010/09/07/transitioning-existing-code-to-the-es5-getter-setter-apis.aspx 75 | // (second clause tests for Object.defineProperty() in IE<9 that only supports extending DOM prototypes, but 76 | // note that IE<9 does not support __defineGetter__ or __defineSetter__ so it just renders the method harmless) 77 | 78 | (function() { 79 | var orig = Object.defineProperty; 80 | var dom_only = !(function(){try{return Object.defineProperty({},'x',{});}catch(_){return false;}}()); 81 | 82 | if (!orig || dom_only) { 83 | Object.defineProperty = function (o, prop, desc) { 84 | // In IE8 try built-in implementation for defining properties on DOM prototypes. 85 | if (orig) 86 | try { return orig(o, prop, desc); } catch (_) {} 87 | if (o !== Object(o)) 88 | throw TypeError('Object.defineProperty called on non-object'); 89 | if (Object.prototype.__defineGetter__ && ('get' in desc)) 90 | Object.prototype.__defineGetter__.call(o, prop, desc.get); 91 | if (Object.prototype.__defineSetter__ && ('set' in desc)) 92 | Object.prototype.__defineSetter__.call(o, prop, desc.set); 93 | if ('value' in desc) 94 | o[prop] = desc.value; 95 | return o; 96 | }; 97 | } 98 | }()); 99 | 100 | // ES5: Make obj[index] an alias for obj._getter(index)/obj._setter(index, value) 101 | // for index in 0 ... obj.length 102 | function makeArrayAccessors(obj) { 103 | if (obj.length > MAX_ARRAY_LENGTH) throw RangeError('Array too large for polyfill'); 104 | 105 | function makeArrayAccessor(index) { 106 | Object.defineProperty(obj, index, { 107 | 'get': function() { return obj._getter(index); }, 108 | 'set': function(v) { obj._setter(index, v); }, 109 | enumerable: true, 110 | configurable: false 111 | }); 112 | } 113 | 114 | var i; 115 | for (i = 0; i < obj.length; i += 1) { 116 | makeArrayAccessor(i); 117 | } 118 | } 119 | 120 | // Internal conversion functions: 121 | // pack() - take a number (interpreted as Type), output a byte array 122 | // unpack() - take a byte array, output a Type-like number 123 | 124 | function as_signed(value, bits) { var s = 32 - bits; return (value << s) >> s; } 125 | function as_unsigned(value, bits) { var s = 32 - bits; return (value << s) >>> s; } 126 | 127 | function packI8(n) { return [n & 0xff]; } 128 | function unpackI8(bytes) { return as_signed(bytes[0], 8); } 129 | 130 | function packU8(n) { return [n & 0xff]; } 131 | function unpackU8(bytes) { return as_unsigned(bytes[0], 8); } 132 | 133 | function packU8Clamped(n) { n = round(Number(n)); return [n < 0 ? 0 : n > 0xff ? 0xff : n & 0xff]; } 134 | 135 | function packI16(n) { return [(n >> 8) & 0xff, n & 0xff]; } 136 | function unpackI16(bytes) { return as_signed(bytes[0] << 8 | bytes[1], 16); } 137 | 138 | function packU16(n) { return [(n >> 8) & 0xff, n & 0xff]; } 139 | function unpackU16(bytes) { return as_unsigned(bytes[0] << 8 | bytes[1], 16); } 140 | 141 | function packI32(n) { return [(n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]; } 142 | function unpackI32(bytes) { return as_signed(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3], 32); } 143 | 144 | function packU32(n) { return [(n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]; } 145 | function unpackU32(bytes) { return as_unsigned(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3], 32); } 146 | 147 | function packIEEE754(v, ebits, fbits) { 148 | 149 | var bias = (1 << (ebits - 1)) - 1, 150 | s, e, f, ln, 151 | i, bits, str, bytes; 152 | 153 | function roundToEven(n) { 154 | var w = floor(n), f = n - w; 155 | if (f < 0.5) 156 | return w; 157 | if (f > 0.5) 158 | return w + 1; 159 | return w % 2 ? w + 1 : w; 160 | } 161 | 162 | // Compute sign, exponent, fraction 163 | if (v !== v) { 164 | // NaN 165 | // http://dev.w3.org/2006/webapi/WebIDL/#es-type-mapping 166 | e = (1 << ebits) - 1; f = pow(2, fbits - 1); s = 0; 167 | } else if (v === Infinity || v === -Infinity) { 168 | e = (1 << ebits) - 1; f = 0; s = (v < 0) ? 1 : 0; 169 | } else if (v === 0) { 170 | e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0; 171 | } else { 172 | s = v < 0; 173 | v = abs(v); 174 | 175 | if (v >= pow(2, 1 - bias)) { 176 | e = min(floor(log(v) / LN2), 1023); 177 | f = roundToEven(v / pow(2, e) * pow(2, fbits)); 178 | if (f / pow(2, fbits) >= 2) { 179 | e = e + 1; 180 | f = 1; 181 | } 182 | if (e > bias) { 183 | // Overflow 184 | e = (1 << ebits) - 1; 185 | f = 0; 186 | } else { 187 | // Normalized 188 | e = e + bias; 189 | f = f - pow(2, fbits); 190 | } 191 | } else { 192 | // Denormalized 193 | e = 0; 194 | f = roundToEven(v / pow(2, 1 - bias - fbits)); 195 | } 196 | } 197 | 198 | // Pack sign, exponent, fraction 199 | bits = []; 200 | for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = floor(f / 2); } 201 | for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = floor(e / 2); } 202 | bits.push(s ? 1 : 0); 203 | bits.reverse(); 204 | str = bits.join(''); 205 | 206 | // Bits to bytes 207 | bytes = []; 208 | while (str.length) { 209 | bytes.push(parseInt(str.substring(0, 8), 2)); 210 | str = str.substring(8); 211 | } 212 | return bytes; 213 | } 214 | 215 | function unpackIEEE754(bytes, ebits, fbits) { 216 | // Bytes to bits 217 | var bits = [], i, j, b, str, 218 | bias, s, e, f; 219 | 220 | for (i = bytes.length; i; i -= 1) { 221 | b = bytes[i - 1]; 222 | for (j = 8; j; j -= 1) { 223 | bits.push(b % 2 ? 1 : 0); b = b >> 1; 224 | } 225 | } 226 | bits.reverse(); 227 | str = bits.join(''); 228 | 229 | // Unpack sign, exponent, fraction 230 | bias = (1 << (ebits - 1)) - 1; 231 | s = parseInt(str.substring(0, 1), 2) ? -1 : 1; 232 | e = parseInt(str.substring(1, 1 + ebits), 2); 233 | f = parseInt(str.substring(1 + ebits), 2); 234 | 235 | // Produce number 236 | if (e === (1 << ebits) - 1) { 237 | return f !== 0 ? NaN : s * Infinity; 238 | } else if (e > 0) { 239 | // Normalized 240 | return s * pow(2, e - bias) * (1 + f / pow(2, fbits)); 241 | } else if (f !== 0) { 242 | // Denormalized 243 | return s * pow(2, -(bias - 1)) * (f / pow(2, fbits)); 244 | } else { 245 | return s < 0 ? -0 : 0; 246 | } 247 | } 248 | 249 | function unpackF64(b) { return unpackIEEE754(b, 11, 52); } 250 | function packF64(v) { return packIEEE754(v, 11, 52); } 251 | function unpackF32(b) { return unpackIEEE754(b, 8, 23); } 252 | function packF32(v) { return packIEEE754(v, 8, 23); } 253 | 254 | // 255 | // 3 The ArrayBuffer Type 256 | // 257 | 258 | (function() { 259 | 260 | function ArrayBuffer(length) { 261 | length = ToInt32(length); 262 | if (length < 0) throw RangeError('ArrayBuffer size is not a small enough positive integer.'); 263 | Object.defineProperty(this, 'byteLength', {value: length}); 264 | Object.defineProperty(this, '_bytes', {value: Array(length)}); 265 | 266 | for (var i = 0; i < length; i += 1) 267 | this._bytes[i] = 0; 268 | } 269 | 270 | global.ArrayBuffer = global.ArrayBuffer || ArrayBuffer; 271 | 272 | // 273 | // 5 The Typed Array View Types 274 | // 275 | 276 | function $TypedArray$() { 277 | 278 | // %TypedArray% ( length ) 279 | if (!arguments.length || typeof arguments[0] !== 'object') { 280 | return (function(length) { 281 | length = ToInt32(length); 282 | if (length < 0) throw RangeError('length is not a small enough positive integer.'); 283 | Object.defineProperty(this, 'length', {value: length}); 284 | Object.defineProperty(this, 'byteLength', {value: length * this.BYTES_PER_ELEMENT}); 285 | Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(this.byteLength)}); 286 | Object.defineProperty(this, 'byteOffset', {value: 0}); 287 | 288 | }).apply(this, arguments); 289 | } 290 | 291 | // %TypedArray% ( typedArray ) 292 | if (arguments.length >= 1 && 293 | Type(arguments[0]) === 'object' && 294 | arguments[0] instanceof $TypedArray$) { 295 | return (function(typedArray){ 296 | if (this.constructor !== typedArray.constructor) throw TypeError(); 297 | 298 | var byteLength = typedArray.length * this.BYTES_PER_ELEMENT; 299 | Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); 300 | Object.defineProperty(this, 'byteLength', {value: byteLength}); 301 | Object.defineProperty(this, 'byteOffset', {value: 0}); 302 | Object.defineProperty(this, 'length', {value: typedArray.length}); 303 | 304 | for (var i = 0; i < this.length; i += 1) 305 | this._setter(i, typedArray._getter(i)); 306 | 307 | }).apply(this, arguments); 308 | } 309 | 310 | // %TypedArray% ( array ) 311 | if (arguments.length >= 1 && 312 | Type(arguments[0]) === 'object' && 313 | !(arguments[0] instanceof $TypedArray$) && 314 | !(arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { 315 | return (function(array) { 316 | 317 | var byteLength = array.length * this.BYTES_PER_ELEMENT; 318 | Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); 319 | Object.defineProperty(this, 'byteLength', {value: byteLength}); 320 | Object.defineProperty(this, 'byteOffset', {value: 0}); 321 | Object.defineProperty(this, 'length', {value: array.length}); 322 | 323 | for (var i = 0; i < this.length; i += 1) { 324 | var s = array[i]; 325 | this._setter(i, Number(s)); 326 | } 327 | }).apply(this, arguments); 328 | } 329 | 330 | // %TypedArray% ( buffer, byteOffset=0, length=undefined ) 331 | if (arguments.length >= 1 && 332 | Type(arguments[0]) === 'object' && 333 | (arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { 334 | return (function(buffer, byteOffset, length) { 335 | 336 | byteOffset = ToUint32(byteOffset); 337 | if (byteOffset > buffer.byteLength) 338 | throw RangeError('byteOffset out of range'); 339 | 340 | // The given byteOffset must be a multiple of the element 341 | // size of the specific type, otherwise an exception is raised. 342 | if (byteOffset % this.BYTES_PER_ELEMENT) 343 | throw RangeError('buffer length minus the byteOffset is not a multiple of the element size.'); 344 | 345 | if (length === undefined) { 346 | var byteLength = buffer.byteLength - byteOffset; 347 | if (byteLength % this.BYTES_PER_ELEMENT) 348 | throw RangeError('length of buffer minus byteOffset not a multiple of the element size'); 349 | length = byteLength / this.BYTES_PER_ELEMENT; 350 | 351 | } else { 352 | length = ToUint32(length); 353 | byteLength = length * this.BYTES_PER_ELEMENT; 354 | } 355 | 356 | if ((byteOffset + byteLength) > buffer.byteLength) 357 | throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); 358 | 359 | Object.defineProperty(this, 'buffer', {value: buffer}); 360 | Object.defineProperty(this, 'byteLength', {value: byteLength}); 361 | Object.defineProperty(this, 'byteOffset', {value: byteOffset}); 362 | Object.defineProperty(this, 'length', {value: length}); 363 | 364 | }).apply(this, arguments); 365 | } 366 | 367 | // %TypedArray% ( all other argument combinations ) 368 | throw TypeError(); 369 | } 370 | 371 | // Properties of the %TypedArray Instrinsic Object 372 | 373 | // %TypedArray%.from ( source , mapfn=undefined, thisArg=undefined ) 374 | Object.defineProperty($TypedArray$, 'from', {value: function(iterable) { 375 | return new this(iterable); 376 | }}); 377 | 378 | // %TypedArray%.of ( ...items ) 379 | Object.defineProperty($TypedArray$, 'of', {value: function(/*...items*/) { 380 | return new this(arguments); 381 | }}); 382 | 383 | // %TypedArray%.prototype 384 | var $TypedArrayPrototype$ = {}; 385 | $TypedArray$.prototype = $TypedArrayPrototype$; 386 | 387 | // WebIDL: getter type (unsigned long index); 388 | Object.defineProperty($TypedArray$.prototype, '_getter', {value: function(index) { 389 | if (arguments.length < 1) throw SyntaxError('Not enough arguments'); 390 | 391 | index = ToUint32(index); 392 | if (index >= this.length) 393 | return undefined; 394 | 395 | var bytes = [], i, o; 396 | for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; 397 | i < this.BYTES_PER_ELEMENT; 398 | i += 1, o += 1) { 399 | bytes.push(this.buffer._bytes[o]); 400 | } 401 | return this._unpack(bytes); 402 | }}); 403 | 404 | // NONSTANDARD: convenience alias for getter: type get(unsigned long index); 405 | Object.defineProperty($TypedArray$.prototype, 'get', {value: $TypedArray$.prototype._getter}); 406 | 407 | // WebIDL: setter void (unsigned long index, type value); 408 | Object.defineProperty($TypedArray$.prototype, '_setter', {value: function(index, value) { 409 | if (arguments.length < 2) throw SyntaxError('Not enough arguments'); 410 | 411 | index = ToUint32(index); 412 | if (index >= this.length) 413 | return; 414 | 415 | var bytes = this._pack(value), i, o; 416 | for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; 417 | i < this.BYTES_PER_ELEMENT; 418 | i += 1, o += 1) { 419 | this.buffer._bytes[o] = bytes[i]; 420 | } 421 | }}); 422 | 423 | // get %TypedArray%.prototype.buffer 424 | // get %TypedArray%.prototype.byteLength 425 | // get %TypedArray%.prototype.byteOffset 426 | // -- applied directly to the object in the constructor 427 | 428 | // %TypedArray%.prototype.constructor 429 | Object.defineProperty($TypedArray$.prototype, 'constructor', {value: $TypedArray$}); 430 | 431 | // %TypedArray%.prototype.copyWithin (target, start, end = this.length ) 432 | Object.defineProperty($TypedArray$.prototype, 'copyWithin', {value: function(target, start) { 433 | var end = arguments[2]; 434 | 435 | var o = ToObject(this); 436 | var lenVal = o.length; 437 | var len = ToUint32(lenVal); 438 | len = max(len, 0); 439 | var relativeTarget = ToInt32(target); 440 | var to; 441 | if (relativeTarget < 0) 442 | to = max(len + relativeTarget, 0); 443 | else 444 | to = min(relativeTarget, len); 445 | var relativeStart = ToInt32(start); 446 | var from; 447 | if (relativeStart < 0) 448 | from = max(len + relativeStart, 0); 449 | else 450 | from = min(relativeStart, len); 451 | var relativeEnd; 452 | if (end === undefined) 453 | relativeEnd = len; 454 | else 455 | relativeEnd = ToInt32(end); 456 | var final; 457 | if (relativeEnd < 0) 458 | final = max(len + relativeEnd, 0); 459 | else 460 | final = min(relativeEnd, len); 461 | var count = min(final - from, len - to); 462 | var direction; 463 | if (from < to && to < from + count) { 464 | direction = -1; 465 | from = from + count - 1; 466 | to = to + count - 1; 467 | } else { 468 | direction = 1; 469 | } 470 | while (count > 0) { 471 | o._setter(to, o._getter(from)); 472 | from = from + direction; 473 | to = to + direction; 474 | count = count - 1; 475 | } 476 | return o; 477 | }}); 478 | 479 | // %TypedArray%.prototype.entries ( ) 480 | // -- defined in es6.js to shim browsers w/ native TypedArrays 481 | 482 | // %TypedArray%.prototype.every ( callbackfn, thisArg = undefined ) 483 | Object.defineProperty($TypedArray$.prototype, 'every', {value: function(callbackfn) { 484 | if (this === undefined || this === null) throw TypeError(); 485 | var t = Object(this); 486 | var len = ToUint32(t.length); 487 | if (!IsCallable(callbackfn)) throw TypeError(); 488 | var thisArg = arguments[1]; 489 | for (var i = 0; i < len; i++) { 490 | if (!callbackfn.call(thisArg, t._getter(i), i, t)) 491 | return false; 492 | } 493 | return true; 494 | }}); 495 | 496 | // %TypedArray%.prototype.fill (value, start = 0, end = this.length ) 497 | Object.defineProperty($TypedArray$.prototype, 'fill', {value: function(value) { 498 | var start = arguments[1], 499 | end = arguments[2]; 500 | 501 | var o = ToObject(this); 502 | var lenVal = o.length; 503 | var len = ToUint32(lenVal); 504 | len = max(len, 0); 505 | var relativeStart = ToInt32(start); 506 | var k; 507 | if (relativeStart < 0) 508 | k = max((len + relativeStart), 0); 509 | else 510 | k = min(relativeStart, len); 511 | var relativeEnd; 512 | if (end === undefined) 513 | relativeEnd = len; 514 | else 515 | relativeEnd = ToInt32(end); 516 | var final; 517 | if (relativeEnd < 0) 518 | final = max((len + relativeEnd), 0); 519 | else 520 | final = min(relativeEnd, len); 521 | while (k < final) { 522 | o._setter(k, value); 523 | k += 1; 524 | } 525 | return o; 526 | }}); 527 | 528 | // %TypedArray%.prototype.filter ( callbackfn, thisArg = undefined ) 529 | Object.defineProperty($TypedArray$.prototype, 'filter', {value: function(callbackfn) { 530 | if (this === undefined || this === null) throw TypeError(); 531 | var t = Object(this); 532 | var len = ToUint32(t.length); 533 | if (!IsCallable(callbackfn)) throw TypeError(); 534 | var res = []; 535 | var thisp = arguments[1]; 536 | for (var i = 0; i < len; i++) { 537 | var val = t._getter(i); // in case fun mutates this 538 | if (callbackfn.call(thisp, val, i, t)) 539 | res.push(val); 540 | } 541 | return new this.constructor(res); 542 | }}); 543 | 544 | // %TypedArray%.prototype.find (predicate, thisArg = undefined) 545 | Object.defineProperty($TypedArray$.prototype, 'find', {value: function(predicate) { 546 | var o = ToObject(this); 547 | var lenValue = o.length; 548 | var len = ToUint32(lenValue); 549 | if (!IsCallable(predicate)) throw TypeError(); 550 | var t = arguments.length > 1 ? arguments[1] : undefined; 551 | var k = 0; 552 | while (k < len) { 553 | var kValue = o._getter(k); 554 | var testResult = predicate.call(t, kValue, k, o); 555 | if (Boolean(testResult)) 556 | return kValue; 557 | ++k; 558 | } 559 | return undefined; 560 | }}); 561 | 562 | // %TypedArray%.prototype.findIndex ( predicate, thisArg = undefined ) 563 | Object.defineProperty($TypedArray$.prototype, 'findIndex', {value: function(predicate) { 564 | var o = ToObject(this); 565 | var lenValue = o.length; 566 | var len = ToUint32(lenValue); 567 | if (!IsCallable(predicate)) throw TypeError(); 568 | var t = arguments.length > 1 ? arguments[1] : undefined; 569 | var k = 0; 570 | while (k < len) { 571 | var kValue = o._getter(k); 572 | var testResult = predicate.call(t, kValue, k, o); 573 | if (Boolean(testResult)) 574 | return k; 575 | ++k; 576 | } 577 | return -1; 578 | }}); 579 | 580 | // %TypedArray%.prototype.forEach ( callbackfn, thisArg = undefined ) 581 | Object.defineProperty($TypedArray$.prototype, 'forEach', {value: function(callbackfn) { 582 | if (this === undefined || this === null) throw TypeError(); 583 | var t = Object(this); 584 | var len = ToUint32(t.length); 585 | if (!IsCallable(callbackfn)) throw TypeError(); 586 | var thisp = arguments[1]; 587 | for (var i = 0; i < len; i++) 588 | callbackfn.call(thisp, t._getter(i), i, t); 589 | }}); 590 | 591 | // %TypedArray%.prototype.indexOf (searchElement, fromIndex = 0 ) 592 | Object.defineProperty($TypedArray$.prototype, 'indexOf', {value: function(searchElement) { 593 | if (this === undefined || this === null) throw TypeError(); 594 | var t = Object(this); 595 | var len = ToUint32(t.length); 596 | if (len === 0) return -1; 597 | var n = 0; 598 | if (arguments.length > 0) { 599 | n = Number(arguments[1]); 600 | if (n !== n) { 601 | n = 0; 602 | } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { 603 | n = (n > 0 || -1) * floor(abs(n)); 604 | } 605 | } 606 | if (n >= len) return -1; 607 | var k = n >= 0 ? n : max(len - abs(n), 0); 608 | for (; k < len; k++) { 609 | if (t._getter(k) === searchElement) { 610 | return k; 611 | } 612 | } 613 | return -1; 614 | }}); 615 | 616 | // %TypedArray%.prototype.join ( separator ) 617 | Object.defineProperty($TypedArray$.prototype, 'join', {value: function(separator) { 618 | if (this === undefined || this === null) throw TypeError(); 619 | var t = Object(this); 620 | var len = ToUint32(t.length); 621 | var tmp = Array(len); 622 | for (var i = 0; i < len; ++i) 623 | tmp[i] = t._getter(i); 624 | return tmp.join(separator === undefined ? ',' : separator); // Hack for IE7 625 | }}); 626 | 627 | // %TypedArray%.prototype.keys ( ) 628 | // -- defined in es6.js to shim browsers w/ native TypedArrays 629 | 630 | // %TypedArray%.prototype.lastIndexOf ( searchElement, fromIndex = this.length-1 ) 631 | Object.defineProperty($TypedArray$.prototype, 'lastIndexOf', {value: function(searchElement) { 632 | if (this === undefined || this === null) throw TypeError(); 633 | var t = Object(this); 634 | var len = ToUint32(t.length); 635 | if (len === 0) return -1; 636 | var n = len; 637 | if (arguments.length > 1) { 638 | n = Number(arguments[1]); 639 | if (n !== n) { 640 | n = 0; 641 | } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { 642 | n = (n > 0 || -1) * floor(abs(n)); 643 | } 644 | } 645 | var k = n >= 0 ? min(n, len - 1) : len - abs(n); 646 | for (; k >= 0; k--) { 647 | if (t._getter(k) === searchElement) 648 | return k; 649 | } 650 | return -1; 651 | }}); 652 | 653 | // get %TypedArray%.prototype.length 654 | // -- applied directly to the object in the constructor 655 | 656 | // %TypedArray%.prototype.map ( callbackfn, thisArg = undefined ) 657 | Object.defineProperty($TypedArray$.prototype, 'map', {value: function(callbackfn) { 658 | if (this === undefined || this === null) throw TypeError(); 659 | var t = Object(this); 660 | var len = ToUint32(t.length); 661 | if (!IsCallable(callbackfn)) throw TypeError(); 662 | var res = []; res.length = len; 663 | var thisp = arguments[1]; 664 | for (var i = 0; i < len; i++) 665 | res[i] = callbackfn.call(thisp, t._getter(i), i, t); 666 | return new this.constructor(res); 667 | }}); 668 | 669 | // %TypedArray%.prototype.reduce ( callbackfn [, initialValue] ) 670 | Object.defineProperty($TypedArray$.prototype, 'reduce', {value: function(callbackfn) { 671 | if (this === undefined || this === null) throw TypeError(); 672 | var t = Object(this); 673 | var len = ToUint32(t.length); 674 | if (!IsCallable(callbackfn)) throw TypeError(); 675 | // no value to return if no initial value and an empty array 676 | if (len === 0 && arguments.length === 1) throw TypeError(); 677 | var k = 0; 678 | var accumulator; 679 | if (arguments.length >= 2) { 680 | accumulator = arguments[1]; 681 | } else { 682 | accumulator = t._getter(k++); 683 | } 684 | while (k < len) { 685 | accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); 686 | k++; 687 | } 688 | return accumulator; 689 | }}); 690 | 691 | // %TypedArray%.prototype.reduceRight ( callbackfn [, initialValue] ) 692 | Object.defineProperty($TypedArray$.prototype, 'reduceRight', {value: function(callbackfn) { 693 | if (this === undefined || this === null) throw TypeError(); 694 | var t = Object(this); 695 | var len = ToUint32(t.length); 696 | if (!IsCallable(callbackfn)) throw TypeError(); 697 | // no value to return if no initial value, empty array 698 | if (len === 0 && arguments.length === 1) throw TypeError(); 699 | var k = len - 1; 700 | var accumulator; 701 | if (arguments.length >= 2) { 702 | accumulator = arguments[1]; 703 | } else { 704 | accumulator = t._getter(k--); 705 | } 706 | while (k >= 0) { 707 | accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); 708 | k--; 709 | } 710 | return accumulator; 711 | }}); 712 | 713 | // %TypedArray%.prototype.reverse ( ) 714 | Object.defineProperty($TypedArray$.prototype, 'reverse', {value: function() { 715 | if (this === undefined || this === null) throw TypeError(); 716 | var t = Object(this); 717 | var len = ToUint32(t.length); 718 | var half = floor(len / 2); 719 | for (var i = 0, j = len - 1; i < half; ++i, --j) { 720 | var tmp = t._getter(i); 721 | t._setter(i, t._getter(j)); 722 | t._setter(j, tmp); 723 | } 724 | return t; 725 | }}); 726 | 727 | // %TypedArray%.prototype.set(array, offset = 0 ) 728 | // %TypedArray%.prototype.set(typedArray, offset = 0 ) 729 | // WebIDL: void set(TypedArray array, optional unsigned long offset); 730 | // WebIDL: void set(sequence array, optional unsigned long offset); 731 | Object.defineProperty($TypedArray$.prototype, 'set', {value: function(index, value) { 732 | if (arguments.length < 1) throw SyntaxError('Not enough arguments'); 733 | var array, sequence, offset, len, 734 | i, s, d, 735 | byteOffset, byteLength, tmp; 736 | 737 | if (typeof arguments[0] === 'object' && arguments[0].constructor === this.constructor) { 738 | // void set(TypedArray array, optional unsigned long offset); 739 | array = arguments[0]; 740 | offset = ToUint32(arguments[1]); 741 | 742 | if (offset + array.length > this.length) { 743 | throw RangeError('Offset plus length of array is out of range'); 744 | } 745 | 746 | byteOffset = this.byteOffset + offset * this.BYTES_PER_ELEMENT; 747 | byteLength = array.length * this.BYTES_PER_ELEMENT; 748 | 749 | if (array.buffer === this.buffer) { 750 | tmp = []; 751 | for (i = 0, s = array.byteOffset; i < byteLength; i += 1, s += 1) { 752 | tmp[i] = array.buffer._bytes[s]; 753 | } 754 | for (i = 0, d = byteOffset; i < byteLength; i += 1, d += 1) { 755 | this.buffer._bytes[d] = tmp[i]; 756 | } 757 | } else { 758 | for (i = 0, s = array.byteOffset, d = byteOffset; 759 | i < byteLength; i += 1, s += 1, d += 1) { 760 | this.buffer._bytes[d] = array.buffer._bytes[s]; 761 | } 762 | } 763 | } else if (typeof arguments[0] === 'object' && typeof arguments[0].length !== 'undefined') { 764 | // void set(sequence array, optional unsigned long offset); 765 | sequence = arguments[0]; 766 | len = ToUint32(sequence.length); 767 | offset = ToUint32(arguments[1]); 768 | 769 | if (offset + len > this.length) { 770 | throw RangeError('Offset plus length of array is out of range'); 771 | } 772 | 773 | for (i = 0; i < len; i += 1) { 774 | s = sequence[i]; 775 | this._setter(offset + i, Number(s)); 776 | } 777 | } else { 778 | throw TypeError('Unexpected argument type(s)'); 779 | } 780 | }}); 781 | 782 | // %TypedArray%.prototype.slice ( start, end ) 783 | Object.defineProperty($TypedArray$.prototype, 'slice', {value: function(start, end) { 784 | var o = ToObject(this); 785 | var lenVal = o.length; 786 | var len = ToUint32(lenVal); 787 | var relativeStart = ToInt32(start); 788 | var k = (relativeStart < 0) ? max(len + relativeStart, 0) : min(relativeStart, len); 789 | var relativeEnd = (end === undefined) ? len : ToInt32(end); 790 | var final = (relativeEnd < 0) ? max(len + relativeEnd, 0) : min(relativeEnd, len); 791 | var count = final - k; 792 | var c = o.constructor; 793 | var a = new c(count); 794 | var n = 0; 795 | while (k < final) { 796 | var kValue = o._getter(k); 797 | a._setter(n, kValue); 798 | ++k; 799 | ++n; 800 | } 801 | return a; 802 | }}); 803 | 804 | // %TypedArray%.prototype.some ( callbackfn, thisArg = undefined ) 805 | Object.defineProperty($TypedArray$.prototype, 'some', {value: function(callbackfn) { 806 | if (this === undefined || this === null) throw TypeError(); 807 | var t = Object(this); 808 | var len = ToUint32(t.length); 809 | if (!IsCallable(callbackfn)) throw TypeError(); 810 | var thisp = arguments[1]; 811 | for (var i = 0; i < len; i++) { 812 | if (callbackfn.call(thisp, t._getter(i), i, t)) { 813 | return true; 814 | } 815 | } 816 | return false; 817 | }}); 818 | 819 | // %TypedArray%.prototype.sort ( comparefn ) 820 | Object.defineProperty($TypedArray$.prototype, 'sort', {value: function(comparefn) { 821 | if (this === undefined || this === null) throw TypeError(); 822 | var t = Object(this); 823 | var len = ToUint32(t.length); 824 | var tmp = Array(len); 825 | for (var i = 0; i < len; ++i) 826 | tmp[i] = t._getter(i); 827 | if (comparefn) tmp.sort(comparefn); else tmp.sort(); // Hack for IE8/9 828 | for (i = 0; i < len; ++i) 829 | t._setter(i, tmp[i]); 830 | return t; 831 | }}); 832 | 833 | // %TypedArray%.prototype.subarray(begin = 0, end = this.length ) 834 | // WebIDL: TypedArray subarray(long begin, optional long end); 835 | Object.defineProperty($TypedArray$.prototype, 'subarray', {value: function(start, end) { 836 | function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } 837 | 838 | start = ToInt32(start); 839 | end = ToInt32(end); 840 | 841 | if (arguments.length < 1) { start = 0; } 842 | if (arguments.length < 2) { end = this.length; } 843 | 844 | if (start < 0) { start = this.length + start; } 845 | if (end < 0) { end = this.length + end; } 846 | 847 | start = clamp(start, 0, this.length); 848 | end = clamp(end, 0, this.length); 849 | 850 | var len = end - start; 851 | if (len < 0) { 852 | len = 0; 853 | } 854 | 855 | return new this.constructor( 856 | this.buffer, this.byteOffset + start * this.BYTES_PER_ELEMENT, len); 857 | }}); 858 | 859 | // %TypedArray%.prototype.toLocaleString ( ) 860 | // %TypedArray%.prototype.toString ( ) 861 | // %TypedArray%.prototype.values ( ) 862 | // %TypedArray%.prototype [ @@iterator ] ( ) 863 | // get %TypedArray%.prototype [ @@toStringTag ] 864 | // -- defined in es6.js to shim browsers w/ native TypedArrays 865 | 866 | function makeTypedArray(elementSize, pack, unpack) { 867 | // Each TypedArray type requires a distinct constructor instance with 868 | // identical logic, which this produces. 869 | var TypedArray = function() { 870 | Object.defineProperty(this, 'constructor', {value: TypedArray}); 871 | $TypedArray$.apply(this, arguments); 872 | makeArrayAccessors(this); 873 | }; 874 | if ('__proto__' in TypedArray) { 875 | TypedArray.__proto__ = $TypedArray$; 876 | } else { 877 | TypedArray.from = $TypedArray$.from; 878 | TypedArray.of = $TypedArray$.of; 879 | } 880 | 881 | TypedArray.BYTES_PER_ELEMENT = elementSize; 882 | 883 | var TypedArrayPrototype = function() {}; 884 | TypedArrayPrototype.prototype = $TypedArrayPrototype$; 885 | 886 | TypedArray.prototype = new TypedArrayPrototype(); 887 | 888 | Object.defineProperty(TypedArray.prototype, 'BYTES_PER_ELEMENT', {value: elementSize}); 889 | Object.defineProperty(TypedArray.prototype, '_pack', {value: pack}); 890 | Object.defineProperty(TypedArray.prototype, '_unpack', {value: unpack}); 891 | 892 | return TypedArray; 893 | } 894 | 895 | var Int8Array = makeTypedArray(1, packI8, unpackI8); 896 | var Uint8Array = makeTypedArray(1, packU8, unpackU8); 897 | var Uint8ClampedArray = makeTypedArray(1, packU8Clamped, unpackU8); 898 | var Int16Array = makeTypedArray(2, packI16, unpackI16); 899 | var Uint16Array = makeTypedArray(2, packU16, unpackU16); 900 | var Int32Array = makeTypedArray(4, packI32, unpackI32); 901 | var Uint32Array = makeTypedArray(4, packU32, unpackU32); 902 | var Float32Array = makeTypedArray(4, packF32, unpackF32); 903 | var Float64Array = makeTypedArray(8, packF64, unpackF64); 904 | 905 | global.Int8Array = global.Int8Array || Int8Array; 906 | global.Uint8Array = global.Uint8Array || Uint8Array; 907 | global.Uint8ClampedArray = global.Uint8ClampedArray || Uint8ClampedArray; 908 | global.Int16Array = global.Int16Array || Int16Array; 909 | global.Uint16Array = global.Uint16Array || Uint16Array; 910 | global.Int32Array = global.Int32Array || Int32Array; 911 | global.Uint32Array = global.Uint32Array || Uint32Array; 912 | global.Float32Array = global.Float32Array || Float32Array; 913 | global.Float64Array = global.Float64Array || Float64Array; 914 | }()); 915 | 916 | // 917 | // 6 The DataView View Type 918 | // 919 | 920 | (function() { 921 | function r(array, index) { 922 | return IsCallable(array.get) ? array.get(index) : array[index]; 923 | } 924 | 925 | var IS_BIG_ENDIAN = (function() { 926 | var u16array = new Uint16Array([0x1234]), 927 | u8array = new Uint8Array(u16array.buffer); 928 | return r(u8array, 0) === 0x12; 929 | }()); 930 | 931 | // DataView(buffer, byteOffset=0, byteLength=undefined) 932 | // WebIDL: Constructor(ArrayBuffer buffer, 933 | // optional unsigned long byteOffset, 934 | // optional unsigned long byteLength) 935 | function DataView(buffer, byteOffset, byteLength) { 936 | if (!(buffer instanceof ArrayBuffer || Class(buffer) === 'ArrayBuffer')) throw TypeError(); 937 | 938 | byteOffset = ToUint32(byteOffset); 939 | if (byteOffset > buffer.byteLength) 940 | throw RangeError('byteOffset out of range'); 941 | 942 | if (byteLength === undefined) 943 | byteLength = buffer.byteLength - byteOffset; 944 | else 945 | byteLength = ToUint32(byteLength); 946 | 947 | if ((byteOffset + byteLength) > buffer.byteLength) 948 | throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); 949 | 950 | Object.defineProperty(this, 'buffer', {value: buffer}); 951 | Object.defineProperty(this, 'byteLength', {value: byteLength}); 952 | Object.defineProperty(this, 'byteOffset', {value: byteOffset}); 953 | }; 954 | 955 | // get DataView.prototype.buffer 956 | // get DataView.prototype.byteLength 957 | // get DataView.prototype.byteOffset 958 | // -- applied directly to instances by the constructor 959 | 960 | function makeGetter(arrayType) { 961 | return function GetViewValue(byteOffset, littleEndian) { 962 | byteOffset = ToUint32(byteOffset); 963 | 964 | if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) 965 | throw RangeError('Array index out of range'); 966 | 967 | byteOffset += this.byteOffset; 968 | 969 | var uint8Array = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT), 970 | bytes = []; 971 | for (var i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) 972 | bytes.push(r(uint8Array, i)); 973 | 974 | if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) 975 | bytes.reverse(); 976 | 977 | return r(new arrayType(new Uint8Array(bytes).buffer), 0); 978 | }; 979 | } 980 | 981 | Object.defineProperty(DataView.prototype, 'getUint8', {value: makeGetter(Uint8Array)}); 982 | Object.defineProperty(DataView.prototype, 'getInt8', {value: makeGetter(Int8Array)}); 983 | Object.defineProperty(DataView.prototype, 'getUint16', {value: makeGetter(Uint16Array)}); 984 | Object.defineProperty(DataView.prototype, 'getInt16', {value: makeGetter(Int16Array)}); 985 | Object.defineProperty(DataView.prototype, 'getUint32', {value: makeGetter(Uint32Array)}); 986 | Object.defineProperty(DataView.prototype, 'getInt32', {value: makeGetter(Int32Array)}); 987 | Object.defineProperty(DataView.prototype, 'getFloat32', {value: makeGetter(Float32Array)}); 988 | Object.defineProperty(DataView.prototype, 'getFloat64', {value: makeGetter(Float64Array)}); 989 | 990 | function makeSetter(arrayType) { 991 | return function SetViewValue(byteOffset, value, littleEndian) { 992 | byteOffset = ToUint32(byteOffset); 993 | if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) 994 | throw RangeError('Array index out of range'); 995 | 996 | // Get bytes 997 | var typeArray = new arrayType([value]), 998 | byteArray = new Uint8Array(typeArray.buffer), 999 | bytes = [], i, byteView; 1000 | 1001 | for (i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) 1002 | bytes.push(r(byteArray, i)); 1003 | 1004 | // Flip if necessary 1005 | if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) 1006 | bytes.reverse(); 1007 | 1008 | // Write them 1009 | byteView = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT); 1010 | byteView.set(bytes); 1011 | }; 1012 | } 1013 | 1014 | Object.defineProperty(DataView.prototype, 'setUint8', {value: makeSetter(Uint8Array)}); 1015 | Object.defineProperty(DataView.prototype, 'setInt8', {value: makeSetter(Int8Array)}); 1016 | Object.defineProperty(DataView.prototype, 'setUint16', {value: makeSetter(Uint16Array)}); 1017 | Object.defineProperty(DataView.prototype, 'setInt16', {value: makeSetter(Int16Array)}); 1018 | Object.defineProperty(DataView.prototype, 'setUint32', {value: makeSetter(Uint32Array)}); 1019 | Object.defineProperty(DataView.prototype, 'setInt32', {value: makeSetter(Int32Array)}); 1020 | Object.defineProperty(DataView.prototype, 'setFloat32', {value: makeSetter(Float32Array)}); 1021 | Object.defineProperty(DataView.prototype, 'setFloat64', {value: makeSetter(Float64Array)}); 1022 | 1023 | global.DataView = global.DataView || DataView; 1024 | 1025 | }()); 1026 | 1027 | }(this)); -------------------------------------------------------------------------------- /karma/test-mac.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration for testing code that interactis with the DOM on 2 | // Mac OS X 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'js/test.js' 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [], 24 | 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 28 | preprocessors: {}, 29 | 30 | 31 | // test results reporter to use 32 | // possible values: 'dots', 'progress' 33 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 34 | reporters: ['progress'], 35 | 36 | 37 | // web server port 38 | port: 9876, 39 | 40 | 41 | // enable / disable colors in the output (reporters and logs) 42 | colors: true, 43 | 44 | 45 | // level of logging 46 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 47 | logLevel: config.LOG_INFO, 48 | 49 | 50 | // enable / disable watching file and executing tests whenever any file changes 51 | autoWatch: false, 52 | 53 | 54 | // start these browsers 55 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 56 | browsers: [ 57 | 'Chrome', 58 | 'Firefox', 59 | 'Safari' 60 | ], 61 | 62 | 63 | // Continuous Integration mode 64 | // if true, Karma captures browsers, runs the tests and exits 65 | singleRun: false 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /karma/test-windows.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration for testing code that interactis with the DOM on 2 | // Windows Operating Systems 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'js/support/polyfill/*.js', // polyfill for older versions of IE 19 | 'js/test.js' 20 | ], 21 | 22 | 23 | // list of files to exclude 24 | exclude: [], 25 | 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: {}, 30 | 31 | 32 | // test results reporter to use 33 | // possible values: 'dots', 'progress' 34 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 35 | reporters: ['progress'], 36 | 37 | 38 | // web server port 39 | port: 9876, 40 | 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | 46 | // level of logging 47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | 51 | // enable / disable watching file and executing tests whenever any file changes 52 | autoWatch: false, 53 | 54 | 55 | // start these browsers 56 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 57 | browsers: [ 58 | 'Chrome', 59 | 'Firefox', 60 | 'IE', 61 | 'IE10', 62 | 'IE9' 63 | ], 64 | 65 | customLaunchers: { 66 | IE10: { 67 | base: 'IE', 68 | 'x-ua-compatible': 'IE=EmulateIE10' 69 | }, 70 | IE9: { 71 | base: 'IE', 72 | 'x-ua-compatible': 'IE=EmulateIE9' 73 | } 74 | }, 75 | 76 | 77 | // Continuous Integration mode 78 | // if true, Karma captures browsers, runs the tests and exits 79 | singleRun: false 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package vdom 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // Parse reads escaped html from src and returns a virtual tree structure 10 | // representing it. It returns an error if there was a problem parsing the html. 11 | func Parse(src []byte) (*Tree, error) { 12 | // Create a xml.Decoder to read from an IndexedByteReader 13 | r := NewIndexedByteReader(src) 14 | dec := xml.NewDecoder(r) 15 | dec.Entity = xml.HTMLEntity 16 | dec.Strict = false 17 | dec.AutoClose = xml.HTMLAutoClose 18 | 19 | // Iterate through each token and construct the tree 20 | tree := &Tree{src: src, reader: r} 21 | var currentParent *Element = nil 22 | for token, err := dec.Token(); ; token, err = dec.Token() { 23 | if err != nil { 24 | if err == io.EOF { 25 | // We reached the end of the document we were parsing 26 | break 27 | } else { 28 | // There was some unexpected error 29 | return nil, err 30 | } 31 | } 32 | if nextParent, err := parseToken(tree, token, currentParent); err != nil { 33 | return nil, err 34 | } else { 35 | currentParent = nextParent 36 | } 37 | } 38 | return tree, nil 39 | } 40 | 41 | // parseToken parses a single token and adds the appropriate node(s) to the tree. When calling 42 | // parseToken iteratively, you should always capture the nextParent return and use it as the 43 | // currentParent argument in the next iteration. 44 | func parseToken(tree *Tree, token xml.Token, currentParent *Element) (nextParent *Element, err error) { 45 | var resultingNode Node 46 | switch token.(type) { 47 | case xml.StartElement: 48 | // Parse the name and attrs directly from the xml.StartElement 49 | startEl := token.(xml.StartElement) 50 | el := &Element{ 51 | Name: parseName(startEl.Name), 52 | tree: tree, 53 | } 54 | for _, attr := range startEl.Attr { 55 | el.Attrs = append(el.Attrs, Attr{ 56 | Name: parseName(attr.Name), 57 | Value: attr.Value, 58 | }) 59 | } 60 | if currentParent != nil { 61 | // Set the index based on how many children we've seen so far 62 | el.index = make([]int, len(currentParent.index)+1) 63 | copy(el.index, currentParent.index) 64 | el.index[len(currentParent.index)] = len(currentParent.children) 65 | // Set this element's parent 66 | el.parent = currentParent 67 | // Add this element to the currentParent's children 68 | currentParent.children = append(currentParent.children, el) 69 | } else { 70 | // There is no current parent, so set the index based on the 71 | // number of first-level child nodes we have seen so far for 72 | // this tree 73 | el.index = []int{len(tree.Children)} 74 | } 75 | // Set the srcStart to indicate where in tree.src the html for this element 76 | // starts. Since we don't know the exact length of the starting tag (might be extra whitespace 77 | // in between attributes), we can't just do arithmetic here. Instead, start from the current 78 | // offset and find the first preceding tag open (the '<' character) 79 | start, err := tree.reader.BackwardsSearch(0, tree.reader.Offset()-1, '<') 80 | if err != nil { 81 | return nil, err 82 | } 83 | el.srcStart = start 84 | // The innerHTML start is just the current offset 85 | el.srcInnerStart = tree.reader.Offset() 86 | // Set this element to the nextParent. The next node(s) we find 87 | // are children of this element until we reach xml.EndElement 88 | nextParent = el 89 | resultingNode = el 90 | case xml.EndElement: 91 | // Assuming the xml is well-formed, this marks the end of the current 92 | // parent 93 | endEl := token.(xml.EndElement) 94 | if currentParent == nil { 95 | // If we reach a closing tag without a corresponding start tag, the 96 | // xml is malformed 97 | return nil, fmt.Errorf("XML was malformed: Found closing tag %s before a corresponding opening tag.", parseName(endEl.Name)) 98 | } else if currentParent.Name != parseName(endEl.Name) { 99 | // Make sure the name of the closing tag matches what we expect 100 | return nil, fmt.Errorf("XML was malformed: Found closing tag %s before the closing tag for %s", parseName(endEl.Name), currentParent.Name) 101 | } 102 | // The currentParent has been closed 103 | // Check whether it was autoclosed 104 | if wasAutoClosed(tree, currentParent.Name) { 105 | // There was not a corresponding closing tag, so the currentParent was 106 | // autoclosed and therefore can have no children. Don't worry about the 107 | // ending index, as our HTML method will do something different in this case. 108 | currentParent.autoClosed = true 109 | } else { 110 | // There was a corresponding closing tag, as indicated by the '/' symbol 111 | // This means we can use the underlying src buffer of the tree to get all 112 | // the bytes for the html of the currentParent and its children. The ending 113 | // index is the current offset. 114 | currentParent.srcEnd = tree.reader.Offset() 115 | // The innerHTML ends at the start of the closing tag 116 | // The closing tag has length of len(currentParent.Name) + 3 117 | // for the <, /, and > characters. 118 | closingTagLength := len(currentParent.Name) + 3 119 | currentParent.srcInnerEnd = tree.reader.Offset() - closingTagLength 120 | } 121 | // The currentParent has no more children. 122 | // The next node(s) we find must be children of currentParent.parent. 123 | if currentParent.parent != nil { 124 | nextParent = currentParent.parent 125 | } else { 126 | nextParent = nil 127 | } 128 | case xml.CharData: 129 | charData := token.(xml.CharData) 130 | // Parse the value from the xml.CharData 131 | text := &Text{ 132 | Value: []byte(charData.Copy()), 133 | } 134 | if currentParent != nil { 135 | // Set the index based on how many children we've seen so far 136 | text.index = make([]int, len(currentParent.index)+1) 137 | copy(text.index, currentParent.index) 138 | text.index[len(currentParent.index)] = len(currentParent.children) 139 | // Set this text node's parent 140 | text.parent = currentParent 141 | // Add this text node to the currentParent's children 142 | currentParent.children = append(currentParent.children, text) 143 | } else { 144 | // There is no current parent, so set the index based on the 145 | // number of first-level child nodes we have seen so far for 146 | // this tree 147 | text.index = []int{len(tree.Children)} 148 | } 149 | resultingNode = text 150 | nextParent = currentParent 151 | case xml.Comment: 152 | xmlComment := token.(xml.Comment) 153 | // Parse the value from the xml.Comment 154 | comment := &Comment{ 155 | Value: []byte(xmlComment.Copy()), 156 | } 157 | if currentParent != nil { 158 | // Set the index based on how many children we've seen so far 159 | comment.index = make([]int, len(currentParent.index)+1) 160 | copy(comment.index, currentParent.index) 161 | comment.index[len(currentParent.index)] = len(currentParent.children) 162 | // Set this comment node's parent 163 | comment.parent = currentParent 164 | // Add this comment node to the currentParent's children 165 | currentParent.children = append(currentParent.children, comment) 166 | } else { 167 | // There is no current parent, so set the index based on the 168 | // number of first-level child nodes we have seen so far for 169 | // this tree 170 | comment.index = []int{len(tree.Children)} 171 | } 172 | resultingNode = comment 173 | nextParent = currentParent 174 | case xml.ProcInst: 175 | return nil, fmt.Errorf("parse error: found token of type xml.ProcInst, which is not allowed in html") 176 | case xml.Directive: 177 | return nil, fmt.Errorf("parse error: found token of type xml.Directive, which is not allowed in html") 178 | } 179 | if resultingNode != nil && currentParent == nil { 180 | // If this node has no parents, it is one of the first-level children 181 | // of the tree. 182 | tree.Children = append(tree.Children, resultingNode) 183 | } 184 | return nextParent, nil 185 | } 186 | 187 | // parseName converts an xml.Name to a single string name. For our 188 | // purposes we are not interested in the different namespaces, and 189 | // just need to treat the name as a single string. 190 | func parseName(name xml.Name) string { 191 | if name.Space != "" { 192 | return fmt.Sprintf("%s:%s", name.Space, name.Local) 193 | } 194 | return name.Local 195 | } 196 | 197 | // wasAutoClosed returns true if the tagName was autoclosed. It does 198 | // this by reading the bytes backwards from the current offset of the 199 | // tree's reader and comparing them to the expected closing tag. 200 | func wasAutoClosed(tree *Tree, tagName string) bool { 201 | closingTag := fmt.Sprintf("", tagName) 202 | stop := tree.reader.Offset() 203 | start := stop - len(closingTag) 204 | if start < 0 { 205 | // The tag must have been autoclosed becuase there's 206 | // not enough space in the buffer before this point 207 | // to contain the entire closingTag. 208 | return true 209 | } 210 | // The tag was autoclosed iff the last bytes to be read 211 | // were not the closing tag. 212 | return string(tree.reader.buf[start:stop]) != closingTag 213 | } 214 | -------------------------------------------------------------------------------- /parse_bench_test.go: -------------------------------------------------------------------------------- 1 | package vdom 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkParse(b *testing.B) { 10 | for i := 0; i < b.N; i++ { 11 | Parse([]byte("
    • one
    • two
    • three
    ")) 12 | } 13 | } 14 | 15 | func BenchmarkXMLDecode(b *testing.B) { 16 | for i := 0; i < b.N; i++ { 17 | buf := bytes.NewBuffer([]byte("
    • one
    • two
    • three
    ")) 18 | dec := xml.NewDecoder(buf) 19 | for _, err := dec.Token(); err == nil; _, err = dec.Token() { 20 | } 21 | } 22 | } 23 | 24 | func BenchmarkDiff(b *testing.B) { 25 | oldTree, _ := Parse([]byte("
    • one
    • two
    • three
    ")) 26 | newTree, _ := Parse([]byte("
    • uno
    • two
    • three
    ")) 27 | for i := 0; i < b.N; i++ { 28 | Diff(oldTree, newTree) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package vdom 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // TestParse tests the tree returned from the Parse function for various different 9 | // inputs. 10 | func TestParse(t *testing.T) { 11 | // We'll use table-driven testing here. 12 | testCases := []struct { 13 | // A human-readable name describing this test case 14 | name string 15 | // The src html to be parsed 16 | src []byte 17 | // The expected tree to be returned from the Parse function 18 | expectedTree *Tree 19 | }{ 20 | { 21 | name: "Element root", 22 | src: []byte("
    "), 23 | expectedTree: &Tree{ 24 | Children: []Node{ 25 | &Element{ 26 | Name: "div", 27 | }, 28 | }, 29 | }, 30 | }, 31 | { 32 | name: "Text root", 33 | src: []byte("Hello"), 34 | expectedTree: &Tree{ 35 | Children: []Node{ 36 | &Text{ 37 | Value: []byte("Hello"), 38 | }, 39 | }, 40 | }, 41 | }, 42 | { 43 | name: "Comment root", 44 | src: []byte(""), 45 | expectedTree: &Tree{ 46 | Children: []Node{ 47 | &Comment{ 48 | Value: []byte("comment"), 49 | }, 50 | }, 51 | }, 52 | }, 53 | { 54 | name: "ul with nested li's", 55 | src: []byte("
    • one
    • two
    • three
    "), 56 | expectedTree: &Tree{ 57 | Children: []Node{ 58 | &Element{ 59 | Name: "ul", 60 | children: []Node{ 61 | &Element{ 62 | Name: "li", 63 | children: []Node{ 64 | &Text{ 65 | Value: []byte("one"), 66 | }, 67 | }, 68 | }, 69 | &Element{ 70 | Name: "li", 71 | children: []Node{ 72 | &Text{ 73 | Value: []byte("two"), 74 | }, 75 | }, 76 | }, 77 | &Element{ 78 | Name: "li", 79 | children: []Node{ 80 | &Text{ 81 | Value: []byte("three"), 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | { 91 | name: "Element with attrs", 92 | src: []byte(`
    `), 93 | expectedTree: &Tree{ 94 | Children: []Node{ 95 | &Element{ 96 | Name: "div", 97 | Attrs: []Attr{ 98 | {Name: "class", Value: "container"}, 99 | {Name: "id", Value: "main"}, 100 | {Name: "data-custom-attr", Value: "foo"}, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | { 107 | name: "Script tag with escaped characters", 108 | src: []byte(``), 109 | expectedTree: &Tree{ 110 | Children: []Node{ 111 | &Element{ 112 | Name: "script", 113 | Attrs: []Attr{ 114 | {Name: "type", Value: "text/javascript"}, 115 | }, 116 | children: []Node{ 117 | &Text{ 118 | Value: []byte(`function((){console.log("")})()`), 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | { 126 | name: "Form with autoclosed tags", 127 | src: []byte(`
    `), 128 | expectedTree: &Tree{ 129 | Children: []Node{ 130 | &Element{ 131 | Name: "form", 132 | Attrs: []Attr{ 133 | {Name: "method", Value: "post"}, 134 | }, 135 | children: []Node{ 136 | &Element{ 137 | Name: "input", 138 | Attrs: []Attr{ 139 | {Name: "type", Value: "text"}, 140 | {Name: "name", Value: "firstName"}, 141 | }, 142 | }, 143 | &Element{ 144 | Name: "input", 145 | Attrs: []Attr{ 146 | {Name: "type", Value: "text"}, 147 | {Name: "name", Value: "lastName"}, 148 | }, 149 | }, 150 | }, 151 | }, 152 | }, 153 | }, 154 | }, 155 | { 156 | name: "Multiple Children", 157 | src: []byte("
    Hello"), 158 | expectedTree: &Tree{ 159 | Children: []Node{ 160 | &Element{ 161 | Name: "div", 162 | }, 163 | &Text{ 164 | Value: []byte("Hello"), 165 | }, 166 | &Comment{ 167 | Value: []byte("comment"), 168 | }, 169 | }, 170 | }, 171 | }, 172 | } 173 | // Iterate through each test case 174 | for i, tc := range testCases { 175 | // Parse the input from tc.src 176 | gotTree, err := Parse(tc.src) 177 | if err != nil { 178 | t.Errorf("Unexpected error in Parse: %s", err.Error()) 179 | } 180 | // Check that the resulting tree matches what we expect 181 | if match, msg := tc.expectedTree.Compare(gotTree, true); !match { 182 | t.Errorf("Error in test case %d (%s): HTML was not parsed correctly.\n%s", i, tc.name, msg) 183 | } 184 | } 185 | } 186 | 187 | // TestHTML tests the HTML method for each node in a parsed tree for various different 188 | // inputs. 189 | func TestHTML(t *testing.T) { 190 | // We'll use table-driven testing here. 191 | testCases := []struct { 192 | // A human-readable name describing this test case 193 | name string 194 | // The src html to be parsed 195 | src []byte 196 | // A function which should check the results of the HTML method of each 197 | // node in the parsed tree, and return an error if any results are incorrect. 198 | testFunc func(*Tree) error 199 | }{ 200 | { 201 | name: "Element root", 202 | src: []byte("
    "), 203 | testFunc: func(tree *Tree) error { 204 | expectedHTML := []byte("
    ") 205 | return expectHTMLEquals(expectedHTML, tree.Children[0].HTML(), "root element") 206 | }, 207 | }, 208 | { 209 | name: "Text root", 210 | src: []byte("Hello"), 211 | testFunc: func(tree *Tree) error { 212 | expectedHTML := []byte("Hello") 213 | return expectHTMLEquals(expectedHTML, tree.Children[0].HTML(), "root text node") 214 | }, 215 | }, 216 | { 217 | name: "Comment root", 218 | src: []byte(""), 219 | testFunc: func(tree *Tree) error { 220 | expectedHTML := []byte("") 221 | return expectHTMLEquals(expectedHTML, tree.Children[0].HTML(), "root comment node") 222 | }, 223 | }, 224 | { 225 | name: "ul with nested li's", 226 | src: []byte("
    • one
    • two
    • three
    "), 227 | testFunc: func(tree *Tree) error { 228 | { 229 | // Test the root of the tree, the ul element 230 | expectedHTML := []byte("
    • one
    • two
    • three
    ") 231 | if err := expectHTMLEquals(expectedHTML, tree.Children[0].HTML(), "the root ul element"); err != nil { 232 | return err 233 | } 234 | } 235 | lis := tree.Children[0].Children() 236 | { 237 | // Test each li element 238 | expectedHTML := [][]byte{ 239 | []byte("
  • one
  • "), 240 | []byte("
  • two
  • "), 241 | []byte("
  • three
  • "), 242 | } 243 | for i, li := range lis { 244 | desc := fmt.Sprintf("li element %d", i) 245 | if err := expectHTMLEquals(expectedHTML[i], li.HTML(), desc); err != nil { 246 | return err 247 | } 248 | } 249 | } 250 | { 251 | // Test each text node inside the li elements 252 | expectedHTML := [][]byte{ 253 | []byte("one"), 254 | []byte("two"), 255 | []byte("three"), 256 | } 257 | for i, li := range lis { 258 | gotHTML := li.Children()[0].HTML() 259 | desc := fmt.Sprintf("the text inside li element %d", i) 260 | if err := expectHTMLEquals(expectedHTML[i], gotHTML, desc); err != nil { 261 | return err 262 | } 263 | } 264 | } 265 | return nil 266 | }, 267 | }, 268 | { 269 | name: "Element with attrs", 270 | src: []byte(`
    `), 271 | testFunc: func(tree *Tree) error { 272 | expectedHTML := []byte(`
    `) 273 | return expectHTMLEquals(expectedHTML, tree.Children[0].HTML(), "root element") 274 | }, 275 | }, 276 | { 277 | name: "Script tag with escaped characters", 278 | src: []byte(``), 279 | testFunc: func(tree *Tree) error { 280 | { 281 | // Test the root element 282 | expectedHTML := []byte(``) 283 | if err := expectHTMLEquals(expectedHTML, tree.Children[0].HTML(), "root script element"); err != nil { 284 | return err 285 | } 286 | } 287 | { 288 | // Test the text node inside the root element 289 | expectedHTML := []byte(`function((){console.log("")})()`) 290 | if err := expectHTMLEquals(expectedHTML, tree.Children[0].Children()[0].HTML(), "text node inside script element"); err != nil { 291 | return err 292 | } 293 | } 294 | return nil 295 | }, 296 | }, 297 | { 298 | name: "Form with autoclosed tags", 299 | src: []byte(`
    `), 300 | testFunc: func(tree *Tree) error { 301 | { 302 | // Test the root element 303 | expectedHTML := []byte(`
    `) 304 | if err := expectHTMLEquals(expectedHTML, tree.Children[0].HTML(), "root script element"); err != nil { 305 | return err 306 | } 307 | } 308 | { 309 | inputs := tree.Children[0].Children() 310 | // Test each child input element 311 | expectedHTML := [][]byte{ 312 | []byte(``), 313 | []byte(``), 314 | } 315 | for i, input := range inputs { 316 | desc := fmt.Sprintf("input element %d", i) 317 | if err := expectHTMLEquals(expectedHTML[i], input.HTML(), desc); err != nil { 318 | return err 319 | } 320 | } 321 | } 322 | return nil 323 | }, 324 | }, 325 | { 326 | name: "Multiple Children", 327 | src: []byte("
    Hello"), 328 | testFunc: func(tree *Tree) error { 329 | expectedHTML := [][]byte{ 330 | []byte(`
    `), 331 | []byte(`Hello`), 332 | []byte(``), 333 | } 334 | for i, root := range tree.Children { 335 | desc := fmt.Sprintf("root node %d of type %T", i, root) 336 | if err := expectHTMLEquals(expectedHTML[i], root.HTML(), desc); err != nil { 337 | return err 338 | } 339 | } 340 | return nil 341 | }, 342 | }, 343 | } 344 | // Iterate through each test case 345 | for i, tc := range testCases { 346 | // Parse the input from tc.src 347 | gotTree, err := Parse(tc.src) 348 | if err != nil { 349 | t.Errorf("Unexpected error in Parse: %s", err.Error()) 350 | } 351 | // Use the testFunc to test for certain conditions 352 | if err := tc.testFunc(gotTree); err != nil { 353 | t.Errorf("Error in test case %d (%s):\n%s", i, tc.name, err.Error()) 354 | } 355 | } 356 | } 357 | 358 | // expectHTMLEquals returns an error if expected does not equal got. description should be 359 | // a human-readable description of the element that was tested. 360 | func expectHTMLEquals(expected []byte, got []byte, description string) error { 361 | if string(expected) != string(got) { 362 | return fmt.Errorf("HTML for %s was not correct.\n\tExpected: %s\n\tBut got: %s", description, string(expected), string(got)) 363 | } 364 | return nil 365 | } 366 | 367 | // TestInnerHTML tests the InnerHTML method for each element in a parsed tree for various different 368 | // inputs. 369 | func TestInnerHTML(t *testing.T) { 370 | // We'll use table-driven testing here. 371 | testCases := []struct { 372 | // A human-readable name describing this test case 373 | name string 374 | // The src html to be parsed 375 | src []byte 376 | // A function which should check the results of the InnerHTML method of each 377 | // node in the parsed tree, and return an error if any results are incorrect. 378 | testFunc func(*Tree) error 379 | }{ 380 | { 381 | name: "Element root", 382 | src: []byte("
    "), 383 | testFunc: func(tree *Tree) error { 384 | expectedInner := []byte("") 385 | el := tree.Children[0].(*Element) 386 | return expectInnerHTMLEquals(expectedInner, el.InnerHTML(), "root element") 387 | }, 388 | }, 389 | { 390 | name: "ul with nested li's", 391 | src: []byte("
    • one
    • two
    • three
    "), 392 | testFunc: func(tree *Tree) error { 393 | { 394 | // Test the root of the tree, the ul element 395 | expectedInner := []byte("
  • one
  • two
  • three
  • ") 396 | el := tree.Children[0].(*Element) 397 | if err := expectInnerHTMLEquals(expectedInner, el.InnerHTML(), "the root ul element"); err != nil { 398 | return err 399 | } 400 | } 401 | lis := tree.Children[0].Children() 402 | { 403 | // Test each li element 404 | expectedInner := [][]byte{ 405 | []byte("one"), 406 | []byte("two"), 407 | []byte("three"), 408 | } 409 | for i, li := range lis { 410 | el := li.(*Element) 411 | desc := fmt.Sprintf("li element %d", i) 412 | if err := expectInnerHTMLEquals(expectedInner[i], el.InnerHTML(), desc); err != nil { 413 | return err 414 | } 415 | } 416 | } 417 | return nil 418 | }, 419 | }, 420 | { 421 | name: "Inner element with attrs", 422 | src: []byte(`
    `), 423 | testFunc: func(tree *Tree) error { 424 | expectedInner := []byte(`
    `) 425 | el := tree.Children[0].(*Element) 426 | return expectInnerHTMLEquals(expectedInner, el.InnerHTML(), "root element") 427 | }, 428 | }, 429 | { 430 | name: "Script tag with escaped characters", 431 | src: []byte(``), 432 | testFunc: func(tree *Tree) error { 433 | expectedInner := []byte(`function((){console.log("")})()`) 434 | el := tree.Children[0].(*Element) 435 | if err := expectInnerHTMLEquals(expectedInner, el.InnerHTML(), "root script element"); err != nil { 436 | return err 437 | } 438 | return nil 439 | }, 440 | }, 441 | { 442 | name: "Form with autoclosed tags", 443 | src: []byte(`
    `), 444 | testFunc: func(tree *Tree) error { 445 | { 446 | // Test the root element 447 | expectedInner := []byte(``) 448 | el := tree.Children[0].(*Element) 449 | if err := expectInnerHTMLEquals(expectedInner, el.InnerHTML(), "root script element"); err != nil { 450 | return err 451 | } 452 | } 453 | { 454 | inputs := tree.Children[0].Children() 455 | // Test each child input element 456 | for i, input := range inputs { 457 | el := input.(*Element) 458 | desc := fmt.Sprintf("input element %d", i) 459 | if err := expectInnerHTMLEquals([]byte{}, el.InnerHTML(), desc); err != nil { 460 | return err 461 | } 462 | } 463 | } 464 | return nil 465 | }, 466 | }, 467 | } 468 | // Iterate through each test case 469 | for i, tc := range testCases { 470 | // Parse the input from tc.src 471 | gotTree, err := Parse(tc.src) 472 | if err != nil { 473 | t.Errorf("Unexpected error in Parse: %s", err.Error()) 474 | } 475 | // Use the testFunc to test for certain conditions 476 | if err := tc.testFunc(gotTree); err != nil { 477 | t.Errorf("Error in test case %d (%s):\n%s", i, tc.name, err.Error()) 478 | } 479 | } 480 | } 481 | 482 | // expectInnerHTMLEquals returns an error if expected does not equal got. description should be 483 | // a human-readable description of the element that was tested. 484 | func expectInnerHTMLEquals(expected []byte, got []byte, description string) error { 485 | if string(expected) != string(got) { 486 | return fmt.Errorf("InnerHTML for %s was not correct.\n\tExpected: %s\n\tBut got: %s", description, string(expected), string(got)) 487 | } 488 | return nil 489 | } 490 | 491 | // TestSelector tests the Selector method for each element in the virtual tree for various 492 | // different inputs. 493 | func TestSelector(t *testing.T) { 494 | // We'll use table-driven testing here. 495 | testCases := []struct { 496 | // A human-readable name describing this test case 497 | name string 498 | // The src html to be parsed 499 | src []byte 500 | // A function which should check the results of the Selector method of each 501 | // element in the parsed tree, and return an error if any results are incorrect. 502 | testFunc func(*Tree) error 503 | }{ 504 | { 505 | name: "Element root", 506 | src: []byte("
    "), 507 | testFunc: func(tree *Tree) error { 508 | el := tree.Children[0].(*Element) 509 | return expectSelectorEquals(el, "*:nth-child(1)", "root element") 510 | }, 511 | }, 512 | { 513 | name: "ul with nested li's", 514 | src: []byte("
    • one
    • two
    • three
    "), 515 | testFunc: func(tree *Tree) error { 516 | { 517 | // Test the root of the tree, the ul element 518 | el := tree.Children[0].(*Element) 519 | if err := expectSelectorEquals(el, "*:nth-child(1)", "the root ul element"); err != nil { 520 | return err 521 | } 522 | } 523 | lis := tree.Children[0].Children() 524 | { 525 | // Test each child li element 526 | for i, li := range lis { 527 | el := li.(*Element) 528 | expected := fmt.Sprintf("*:nth-child(1) > *:nth-child(%d)", i+1) 529 | desc := fmt.Sprintf("li element %d", i) 530 | if err := expectSelectorEquals(el, expected, desc); err != nil { 531 | return err 532 | } 533 | } 534 | } 535 | return nil 536 | }, 537 | }, 538 | { 539 | name: "Form with autoclosed tags", 540 | src: []byte(`
    `), 541 | testFunc: func(tree *Tree) error { 542 | { 543 | // Test the root element 544 | el := tree.Children[0].(*Element) 545 | if err := expectSelectorEquals(el, "*:nth-child(1)", "the root ul element"); err != nil { 546 | return err 547 | } 548 | } 549 | { 550 | inputs := tree.Children[0].Children() 551 | // Test each child input element 552 | for i, input := range inputs { 553 | el := input.(*Element) 554 | expected := fmt.Sprintf("*:nth-child(1) > *:nth-child(%d)", i+1) 555 | desc := fmt.Sprintf("input element %d", i) 556 | if err := expectSelectorEquals(el, expected, desc); err != nil { 557 | return err 558 | } 559 | } 560 | } 561 | return nil 562 | }, 563 | }, 564 | } 565 | // Iterate through each test case 566 | for i, tc := range testCases { 567 | // Parse the input from tc.src 568 | gotTree, err := Parse(tc.src) 569 | if err != nil { 570 | t.Errorf("Unexpected error in Parse: %s", err.Error()) 571 | } 572 | // Use the testFunc to test for certain conditions 573 | if err := tc.testFunc(gotTree); err != nil { 574 | t.Errorf("Error in test case %d (%s):\n%s", i, tc.name, err.Error()) 575 | } 576 | } 577 | } 578 | 579 | // expectSelectorEquals returns an error if el.Selector() does not equal expected. description 580 | // should be a human-readable description of the element that was tested, e.g. "the root ul element" 581 | func expectSelectorEquals(el *Element, expected, description string) error { 582 | got := el.Selector() 583 | if expected != got { 584 | return fmt.Errorf("Selector for %s was not correct. Expected `%s` but got `%s`", description, expected, got) 585 | } 586 | return nil 587 | } 588 | -------------------------------------------------------------------------------- /patch.go: -------------------------------------------------------------------------------- 1 | package vdom 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gopherjs/gopherjs/js" 6 | "honnef.co/go/js/dom" 7 | ) 8 | 9 | var document dom.Document 10 | 11 | func init() { 12 | // We only want to initialize document if we are running in the browser. 13 | // We can detect this by checking if the document is defined. 14 | if js.Global != nil && js.Global.Get("document") != js.Undefined { 15 | document = dom.GetWindow().Document() 16 | } 17 | } 18 | 19 | // Patcher represents changes that can be made to the DOM. 20 | type Patcher interface { 21 | // Patch applies the given patch to the DOM. The given root 22 | // is a relative starting point for the virtual tree in the 23 | // actual DOM. 24 | Patch(root dom.Element) error 25 | } 26 | 27 | // PatchSet is a set of zero or more Patchers 28 | type PatchSet []Patcher 29 | 30 | // Patch satisfies the Patcher interface and sequentially applies 31 | // all the patches in the patch set. 32 | func (ps PatchSet) Patch(root dom.Element) error { 33 | for _, patch := range ps { 34 | if err := patch.Patch(root); err != nil { 35 | return err 36 | } 37 | } 38 | return nil 39 | } 40 | 41 | // Append is a Patcher which will append a child Node to a parent Node. 42 | type Append struct { 43 | Child Node 44 | Parent *Element 45 | } 46 | 47 | // Patch satisfies the Patcher interface and applies the change to the 48 | // actual DOM. 49 | func (p *Append) Patch(root dom.Element) error { 50 | // fmt.Println("Got parent: ", p.Parent) 51 | // fmt.Println("Parent == nil: ", p.Parent == nil) 52 | // fmt.Println("Parent != nil: ", p.Parent != nil) 53 | var parent dom.Node 54 | if p.Parent != nil { 55 | // fmt.Println("Finding parent in DOM") 56 | parent = findInDOM(p.Parent, root) 57 | } else { 58 | // fmt.Println("Setting parent as root") 59 | parent = root 60 | } 61 | // fmt.Println("Computed parent: ", parent) 62 | child := createForDOM(p.Child) 63 | // fmt.Println("Created child: ", child) 64 | parent.AppendChild(child) 65 | // fmt.Println("Successfully appended") 66 | return nil 67 | } 68 | 69 | // Replace is a Patcher will will replace an old Node with a new Node. 70 | type Replace struct { 71 | Old Node 72 | New Node 73 | } 74 | 75 | // Patch satisfies the Patcher interface and applies the change to the 76 | // actual DOM. 77 | func (p *Replace) Patch(root dom.Element) error { 78 | var parent dom.Node 79 | if p.Old.Parent() != nil { 80 | parent = findInDOM(p.Old.Parent(), root) 81 | } else { 82 | parent = root 83 | } 84 | oldChild := findInDOM(p.Old, root) 85 | newChild := createForDOM(p.New) 86 | parent.ReplaceChild(newChild, oldChild) 87 | return nil 88 | } 89 | 90 | // Remove is a Patcher which will remove the given Node. 91 | type Remove struct { 92 | Node Node 93 | } 94 | 95 | // Patch satisfies the Patcher interface and applies the change to the 96 | // actual DOM. 97 | func (p *Remove) Patch(root dom.Element) error { 98 | var parent dom.Node 99 | if p.Node.Parent() != nil { 100 | parent = findInDOM(p.Node.Parent(), root) 101 | } else { 102 | parent = root 103 | } 104 | self := findInDOM(p.Node, root) 105 | parent.RemoveChild(self) 106 | 107 | // p.Node was removed, so subtract one from the final index for all 108 | // siblings that come after it. 109 | if p.Node.Parent() != nil { 110 | lastIndex := p.Node.Index()[len(p.Node.Index())-1] 111 | for _, sibling := range p.Node.Parent().Children()[lastIndex:] { 112 | switch t := sibling.(type) { 113 | case *Element: 114 | t.index[len(t.index)-1] = t.index[len(t.index)-1] - 1 115 | case *Text: 116 | t.index[len(t.index)-1] = t.index[len(t.index)-1] - 1 117 | case *Comment: 118 | t.index[len(t.index)-1] = t.index[len(t.index)-1] - 1 119 | default: 120 | panic("unreachable") 121 | } 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // SettAttr is a Patcher which will set the attribute of the given Node to 129 | // the given Attr. It will overwrite any previous values for the given Attr. 130 | type SetAttr struct { 131 | Node Node 132 | Attr *Attr 133 | } 134 | 135 | // Patch satisfies the Patcher interface and applies the change to the 136 | // actual DOM. 137 | func (p *SetAttr) Patch(root dom.Element) error { 138 | self := findInDOM(p.Node, root).(dom.Element) 139 | self.SetAttribute(p.Attr.Name, p.Attr.Value) 140 | return nil 141 | } 142 | 143 | // RemoveAttr is a Patcher which will remove the attribute with the given 144 | // name from the given Node. 145 | type RemoveAttr struct { 146 | Node Node 147 | AttrName string 148 | } 149 | 150 | // Patch satisfies the Patcher interface and applies the change to the 151 | // actual DOM. 152 | func (p *RemoveAttr) Patch(root dom.Element) error { 153 | self := findInDOM(p.Node, root).(dom.Element) 154 | self.RemoveAttribute(p.AttrName) 155 | return nil 156 | } 157 | 158 | // findInDOM finds the node in the actual DOM corresponding 159 | // to the given virtual node, using the given root as a relative 160 | // starting point. 161 | func findInDOM(node Node, root dom.Element) dom.Node { 162 | el := root.ChildNodes()[node.Index()[0]] 163 | for _, i := range node.Index()[1:] { 164 | el = el.ChildNodes()[i] 165 | } 166 | return el 167 | } 168 | 169 | // createForDOM creates a real node corresponding to the given 170 | // virtual node. It does not insert it into the actual DOM. 171 | func createForDOM(node Node) dom.Node { 172 | switch node.(type) { 173 | case *Element: 174 | vEl := node.(*Element) 175 | el := document.CreateElement(vEl.Name) 176 | for _, attr := range vEl.Attrs { 177 | el.SetAttribute(attr.Name, attr.Value) 178 | } 179 | el.SetInnerHTML(string(vEl.InnerHTML())) 180 | return el 181 | case *Text: 182 | vText := node.(*Text) 183 | textNode := document.CreateTextNode(string(vText.Value)) 184 | return textNode 185 | case *Comment: 186 | vComment := node.(*Comment) 187 | commentNode := document.Underlying().Call("createComment", string(vComment.Value)) 188 | return dom.WrapNode(commentNode) 189 | default: 190 | msg := fmt.Sprintf("Don't know how to create node for type %T", node) 191 | panic(msg) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /scripts/bench.sh: -------------------------------------------------------------------------------- 1 | echo "--> running go benchmarks..." 2 | go test . -bench . -run none | sed 's/^/ /' 3 | echo "--> running gopherjs benchmarks..." 4 | gopherjs test github.com/albrow/vdom --bench=. --run=none | sed 's/^/ /' 5 | echo " compiling karma benchmarks to js..." 6 | gopherjs build karma/go/bench.go -o karma/js/bench.js | sed 's/^/ /' 7 | echo " running benchmarks with karma..." 8 | karma run | sed 's/^/ /' 9 | echo "DONE." 10 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | echo "--> running go tests..." 2 | go test . | sed 's/^/ /' 3 | echo "--> running gopherjs tests..." 4 | gopherjs test github.com/albrow/vdom | sed 's/^/ /' 5 | echo "--> running karma tests..." 6 | echo " compiling karma tests to js..." 7 | gopherjs build karma/go/test.go -o karma/js/test.js | sed 's/^/ /' 8 | echo " running tests with karma..." 9 | karma run | sed 's/^/ /' 10 | echo "DONE." 11 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package vdom 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "reflect" 7 | ) 8 | 9 | // A Tree is a virtual, in-memory representation of a DOM tree 10 | type Tree struct { 11 | // Children is the first-level child nodes for the tree 12 | Children []Node 13 | reader *IndexedByteReader 14 | src []byte 15 | } 16 | 17 | // HTML returns the html of this tree and recursively its children 18 | // as a slice of bytes. 19 | func (t *Tree) HTML() []byte { 20 | escaped := string(t.src) 21 | return []byte(html.UnescapeString(escaped)) 22 | } 23 | 24 | // A Node is an element inside a tree. 25 | type Node interface { 26 | // Parent returns the parent node or nil if there is none 27 | Parent() *Element 28 | // Children returns a slice of child nodes or nil if there 29 | // are none 30 | Children() []Node 31 | // HTML returns the unescaped html of this node and its 32 | // children as a slice of bytes. 33 | HTML() []byte 34 | // Index returns the child indexes starting at the root of the 35 | // virtual tree that can be used to get to this node. So if this 36 | // node is the second child of its parent, and its parent is the first 37 | // child of some root node, Index should return [0, 1]. This means we 38 | // can get to this node via root.ChildNodes()[0].ChildNodes()[1]. 39 | Index() []int 40 | } 41 | 42 | // Attr is an html attribute 43 | type Attr struct { 44 | Name string 45 | Value string 46 | } 47 | 48 | // Element is an html element, e.g.,
    . Name does not include the 49 | // <, >, or / symbols. 50 | type Element struct { 51 | Name string 52 | Attrs []Attr 53 | parent *Element 54 | children []Node 55 | tree *Tree 56 | srcStart int 57 | srcEnd int 58 | srcInnerStart int 59 | srcInnerEnd int 60 | autoClosed bool 61 | index []int 62 | } 63 | 64 | func (e *Element) Parent() *Element { 65 | return e.parent 66 | } 67 | 68 | func (e *Element) Children() []Node { 69 | return e.children 70 | } 71 | 72 | func (e *Element) HTML() []byte { 73 | if e.autoClosed { 74 | // If the tag was autoclosed, it has no children. Just construct the html manually 75 | result := []byte(fmt.Sprintf("<%s", e.Name)) 76 | for _, attr := range e.Attrs { 77 | result = append(result, []byte(fmt.Sprintf(` %s="%s"`, attr.Name, attr.Value))...) 78 | } 79 | result = append(result, '>') 80 | return result 81 | } else { 82 | escaped := string(e.tree.src[e.srcStart:e.srcEnd]) 83 | return []byte(html.UnescapeString(escaped)) 84 | } 85 | } 86 | 87 | // AttrMap returns this element's attributes as a map 88 | // of attribute name to attribute value 89 | func (e *Element) AttrMap() map[string]string { 90 | m := map[string]string{} 91 | for _, attr := range e.Attrs { 92 | m[attr.Name] = attr.Value 93 | } 94 | return m 95 | } 96 | 97 | // InnerHTML returns the unescaped html inside of e. So if e 98 | // is
    • one
    • two
    , it will return 99 | //
  • one
  • two
  • . Since Element is the only type that 100 | // can have children, this only makes sense for the Element type. 101 | func (e *Element) InnerHTML() []byte { 102 | if e.autoClosed { 103 | // If the tag was autoclosed, it has no children, and therefore no inner html. 104 | return nil 105 | } else { 106 | escaped := string(e.tree.src[e.srcInnerStart:e.srcInnerEnd]) 107 | return []byte(html.UnescapeString(escaped)) 108 | } 109 | } 110 | 111 | // Selector returns a css selector which can be used to find 112 | // the corresponding element in the actual DOM. The selector 113 | // should be applied to the root of the tree, i.e. the starting 114 | // point for the virtual tree in the actual DOM. 115 | func (e *Element) Selector() string { 116 | // Simply use the index field to construct a selector with nth-child. 117 | selector := fmt.Sprintf("*:nth-child(%d)", e.index[0]+1) 118 | for _, i := range e.index[1:] { 119 | selector += fmt.Sprintf(" > *:nth-child(%d)", i+1) 120 | } 121 | return selector 122 | } 123 | 124 | func (e *Element) Index() []int { 125 | return e.index 126 | } 127 | 128 | // Compare non-recursively compares e to other. It does not check 129 | // the child nodes since they can be a Node with any underlying type. 130 | // If you want to compare the parent and children fields, use CompareNodes. 131 | func (e *Element) Compare(other *Element, compareAttrs bool) (bool, string) { 132 | if e.Name != other.Name { 133 | return false, fmt.Sprintf("e.Name was %s but other.Name was %s", e.Name, other.Name) 134 | } 135 | if !compareAttrs { 136 | return true, "" 137 | } 138 | attrs := e.Attrs 139 | otherAttrs := other.Attrs 140 | if len(attrs) != len(otherAttrs) { 141 | return false, fmt.Sprintf("n has %d attrs but other has %d attrs.", len(attrs), len(otherAttrs)) 142 | } 143 | for i, attr := range attrs { 144 | otherAttr := otherAttrs[i] 145 | if attr != otherAttr { 146 | return false, fmt.Sprintf("e.Attrs[%d] was %s but other.Attrs[%d] was %s", i, attr, i, otherAttr) 147 | } 148 | } 149 | return true, "" 150 | } 151 | 152 | // Text is a text node inside an xml/html document, i.e. anything 153 | // not surrounded by tags. 154 | type Text struct { 155 | Value []byte 156 | parent *Element 157 | index []int 158 | } 159 | 160 | func (t *Text) Parent() *Element { 161 | return t.parent 162 | } 163 | 164 | func (t *Text) Children() []Node { 165 | // A text node can't have any children 166 | return nil 167 | } 168 | 169 | func (t *Text) HTML() []byte { 170 | return t.Value 171 | } 172 | 173 | func (t *Text) Index() []int { 174 | return t.index 175 | } 176 | 177 | // Compare non-recursively compares t to other. It does not check 178 | // the child nodes since they can be a Node with any underlying type. 179 | // If you want to compare the parent and children fields, use CompareNodes. 180 | func (t *Text) Compare(other *Text) (bool, string) { 181 | if string(t.Value) != string(other.Value) { 182 | return false, fmt.Sprintf("t.Value was %s but other.Value was %s", string(t.Value), string(other.Value)) 183 | } 184 | return true, "" 185 | } 186 | 187 | // Comment is an xml/html comment of the form . 188 | // Value does not include the markers. 189 | type Comment struct { 190 | Value []byte 191 | parent *Element 192 | index []int 193 | } 194 | 195 | func (c *Comment) Parent() *Element { 196 | return c.parent 197 | } 198 | 199 | func (c *Comment) Children() []Node { 200 | // A commet node can't have any children 201 | return nil 202 | } 203 | 204 | func (c *Comment) HTML() []byte { 205 | // Re-add the open and close for the tag 206 | result := []byte("")...) 209 | return result 210 | } 211 | 212 | func (c *Comment) Index() []int { 213 | return c.index 214 | } 215 | 216 | // Compare non-recursively compares c to other. It does not check 217 | // the child nodes since they can be a Node with any underlying type. 218 | // If you want to compare the parent and children fields, use CompareNodes. 219 | func (c *Comment) Compare(other *Comment) (bool, string) { 220 | if string(c.Value) != string(other.Value) { 221 | return false, fmt.Sprintf("c.Value was %s but other.Value was %s", string(c.Value), string(other.Value)) 222 | } 223 | return true, "" 224 | } 225 | 226 | // Compare recursively compares t to other. It returns false and a detailed 227 | // message if n does not equal other. Otherwise, it returns true and an empty 228 | // string. NOTE: Comare never checks the parent properties of t's 229 | // children. This is so you can construct a comparable tree inside a literal. 230 | // (You can't set the parent field inside a literal). 231 | func (t *Tree) Compare(other *Tree, compareAttrs bool) (bool, string) { 232 | if len(t.Children) != len(other.Children) { 233 | return false, fmt.Sprintf("t had %d first-level children but other had %d", len(t.Children), len(other.Children)) 234 | } 235 | for i, root := range t.Children { 236 | otherRoot := other.Children[i] 237 | if match, msg := CompareNodesRecursive(root, otherRoot, compareAttrs); !match { 238 | return false, msg 239 | } 240 | } 241 | return true, "" 242 | } 243 | 244 | // CompareNodes non-recursively compares n to other. It returns false and 245 | // a detailed message if n does not equal other. Otherwise, it returns true and 246 | // an empty string. NOTE: CompareNodes never checks the parent properties of n 247 | // or n's children. This is so you can construct a comparable tree inside a literal. 248 | // (You can't set the parent field inside a literal). 249 | func CompareNodes(n Node, other Node, compareAttrs bool) (bool, string) { 250 | if reflect.TypeOf(n) != reflect.TypeOf(other) { 251 | return false, fmt.Sprintf("n has underlying type %T but the other node has underlying type %T", n, other) 252 | } 253 | switch n.(type) { 254 | case *Element: 255 | el := n.(*Element) 256 | otherEl := other.(*Element) 257 | if match, msg := el.Compare(otherEl, compareAttrs); !match { 258 | return false, msg 259 | } 260 | case *Text: 261 | text := n.(*Text) 262 | otherText := other.(*Text) 263 | if match, msg := text.Compare(otherText); !match { 264 | return false, msg 265 | } 266 | case *Comment: 267 | comment := n.(*Comment) 268 | otherComment := other.(*Comment) 269 | if match, msg := comment.Compare(otherComment); !match { 270 | return false, msg 271 | } 272 | default: 273 | return false, fmt.Sprintf("Don't know how to compare n of underlying type %T", n) 274 | } 275 | return true, "" 276 | } 277 | 278 | // CompareNodesRecursive recursively compares n to other. It returns false and 279 | // a detailed message if n does not equal other. Otherwise, it returns true and 280 | // an empty string. NOTE: CompareNodesRecursive never checks the parent properties 281 | // of n or n's children. This is so you can construct a comparable tree inside a 282 | // literal. (You can't set the parent field inside a literal). 283 | func CompareNodesRecursive(n Node, other Node, compareAttrs bool) (bool, string) { 284 | if match, msg := CompareNodes(n, other, compareAttrs); !match { 285 | return false, msg 286 | } 287 | children := n.Children() 288 | otherChildren := other.Children() 289 | if len(children) != len(otherChildren) { 290 | return false, fmt.Sprintf("n has %d children but other has %d children.", len(children), len(otherChildren)) 291 | } 292 | for i, child := range children { 293 | otherChild := otherChildren[i] 294 | if match, msg := CompareNodesRecursive(child, otherChild, compareAttrs); !match { 295 | return false, msg 296 | } 297 | } 298 | return true, "" 299 | } 300 | --------------------------------------------------------------------------------