Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
brendonmatos committed Jun 15, 2021
0 parents commit dc1e191
Show file tree
Hide file tree
Showing 5 changed files with 698 additions and 0 deletions.
250 changes: 250 additions & 0 deletions diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package diff

import (
"golang.org/x/net/html"
"strconv"
)

type Type int

func (dt Type) toString() string {
return strconv.Itoa(int(dt))
}

const (
Append Type = iota
Remove
SetInnerHTML
SetAttr
RemoveAttr
Replace
Move
)

type changeInstruction struct {
changeType Type
element *html.Node
content string
attr attrChange
index int
}

// attrChange todo
type attrChange struct {
name string
value string
}

type diff struct {
actual *html.Node
instructions []changeInstruction
quantity int
doneElements []*html.Node
}

func newDiff(actual *html.Node) *diff {
return &diff{
actual: actual,
instructions: make([]changeInstruction, 0),
}
}

func (d *diff) instructionsByType(t Type) []changeInstruction {
s := make([]changeInstruction, 0)

for _, i := range d.instructions {
if i.changeType == t {
s = append(s, i)
}
}

return s
}

func (d *diff) checkpoint() {
d.quantity = len(d.instructions)
}

// Has changed since last checkpoint
func (d *diff) hasChanged() bool {
return len(d.instructions) != d.quantity
}

func (d *diff) propose(proposed *html.Node) {
d.clearMarked()
d.diffNode(d.actual, proposed)
}

func (d *diff) diffNode(actual, proposed *html.Node) {

if actual == nil || proposed == nil {
d.diffWalk(actual, proposed)
return
}

uidActual, actualOk := getLiveUidAttributeValue(actual)
uidProposed, proposedOk := getLiveUidAttributeValue(proposed)

if actualOk && proposedOk && uidActual != uidProposed {
content, _ := renderNodeToString(proposed)
d.instructions = append(d.instructions, changeInstruction{
changeType: Replace,
element: actual,
content: content,
})
return
}

d.diffNodeAttributes(actual, proposed)
d.diffWalk(actual.FirstChild, proposed.FirstChild)
d.markNodeDone(proposed)
}

func (d *diff) clearMarked() {
d.doneElements = make([]*html.Node, 0)
}

func (d *diff) markNodeDone(node *html.Node) {
d.doneElements = append(d.doneElements, node)
}

func (d *diff) isMarked(node *html.Node) bool {
for _, n := range d.doneElements {
if n == node {
return true
}
}

return false
}

func (d *diff) diffWalk(actual, proposed *html.Node) {

if actual == nil && proposed == nil {
return
}

if nodeIsText(actual) || nodeIsText(proposed) {
d.checkpoint()
d.diffTextNode(actual, proposed)
if d.hasChanged() {
return
}
}

if actual != nil && proposed != nil {
d.diffNode(actual, proposed)
} else if actual == nil && nodeIsElement(proposed) {
nodeContent, _ := renderNodeToString(proposed)
d.instructions = append(d.instructions, changeInstruction{
changeType: Append,
element: proposed.Parent,
content: nodeContent,
})
d.markNodeDone(proposed)
} else if proposed == nil && nodeIsElement(actual) {
d.instructions = append(d.instructions, changeInstruction{
changeType: Remove,
element: actual,
})
d.markNodeDone(actual)
}

nextActual := nextRelevantElement(actual)
nextProposed := nextRelevantElement(proposed)

if nextActual != nil || nextProposed != nil {
d.diffWalk(nextActual, nextProposed)
}
}

func (d *diff) forceRenderElementContent(proposed *html.Node) {
childrenHTML, _ := renderInnerHTML(proposed)

d.instructions = append(d.instructions, changeInstruction{
changeType: SetInnerHTML,
content: childrenHTML,
element: proposed,
})
}

// diffNodeAttributes compares the attributes in el to the attributes in otherEl
// and adds the necessary patches to make the attributes in el match those in
// otherEl
func (d *diff) diffNodeAttributes(actual, proposed *html.Node) {

actualAttrs := AttrMapFromNode(actual)
proposedAttrs := AttrMapFromNode(proposed)

// Now iterate through the attributes in otherEl
for name, otherValue := range proposedAttrs {
value, found := actualAttrs[name]
if !found || value != otherValue {
d.instructions = append(d.instructions, changeInstruction{
changeType: SetAttr,
element: actual,
attr: attrChange{
name: name,
value: otherValue,
},
})
}
}

for attrName := range actualAttrs {
if _, found := proposedAttrs[attrName]; !found {

d.instructions = append(d.instructions, changeInstruction{
changeType: RemoveAttr,
element: actual,
attr: attrChange{
name: attrName,
},
})
}
}
}

func (d *diff) diffTextNode(actual, proposed *html.Node) {

// Any node is text
if !nodeIsText(proposed) && !nodeIsText(actual) {
return
}

proposedIsRelevant := nodeRelevant(proposed)
actualIsRelevant := nodeRelevant(actual)

if !proposedIsRelevant && !actualIsRelevant {
return
}

// XOR
if proposedIsRelevant != actualIsRelevant {
goto renderEntireNode
}

if proposed.Data != actual.Data {
goto renderEntireNode
}

return

renderEntireNode:
{

node := proposed

if node == nil {
node = actual
}

if node == nil {
return
}

d.forceRenderElementContent(node.Parent)
d.markNodeDone(node.Parent)
}

}
63 changes: 63 additions & 0 deletions dom_selector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package diff

import "strings"

type domSelector struct {
query []*domElemSelector
}

func newDomSelector() *domSelector {
return &domSelector{
query: make([]*domElemSelector, 0),
}
}

func (ds *domSelector) addChild() *domElemSelector {
de := newDOMElementSelector()
ds.addChildSelector(de)
return de
}

func (ds *domSelector) addParentSelector(d *domElemSelector) {
ds.query = append([]*domElemSelector{d}, ds.query...)
}

func (ds *domSelector) addChildSelector(d *domElemSelector) {
ds.query = append(ds.query, d)
}
func (ds *domSelector) addParent() *domElemSelector {
de := newDOMElementSelector()
ds.addParentSelector(de)
return de
}

func (ds *domSelector) toString() string {
var e []string

for _, q := range ds.query {
e = append(e, q.toString())
}

return strings.Join(e, " ")
}

type domElemSelector struct {
query []string
}

func newDOMElementSelector() *domElemSelector {
return &domElemSelector{
query: []string{},
}
}

func (de *domElemSelector) setElemen(elemn string) {
de.query = append(de.query, elemn)
}

func (de *domElemSelector) addAttr(key, value string) {
de.query = append(de.query, "[", key, "=\"", value, "\"]")
}
func (de *domElemSelector) toString() string {
return strings.Join(de.query, "")
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/brendonmatos/diff

go 1.13

require golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
Loading

0 comments on commit dc1e191

Please sign in to comment.