mirror of
				https://github.com/MontFerret/ferret.git
				synced 2025-10-30 23:37:40 +02:00 
			
		
		
		
	Feature/#250 wait style (#255)
* Added support for parsed styles * Added stdlib function. * Added e2e tests * Added e2e tests for STYLE_* functions
This commit is contained in:
		
							
								
								
									
										9
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
								
							| @@ -65,6 +65,14 @@ | ||||
|   pruneopts = "UT" | ||||
|   revision = "d547d1d9531ed93dbdebcbff7f83e7c876a1e0ee" | ||||
|  | ||||
| [[projects]] | ||||
|   digest = "1:fc51ecee8f31d03436c1a0167eb1e383ad0a241d02272541853f3995374a08f1" | ||||
|   name = "github.com/gorilla/css" | ||||
|   packages = ["scanner"] | ||||
|   pruneopts = "UT" | ||||
|   revision = "398b0b046082ecb3694c01bec6b336a06a4e530a" | ||||
|   version = "v1.0.0" | ||||
|  | ||||
| [[projects]] | ||||
|   digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d" | ||||
|   name = "github.com/gorilla/websocket" | ||||
| @@ -297,6 +305,7 @@ | ||||
|     "github.com/corpix/uarand", | ||||
|     "github.com/derekparker/trie", | ||||
|     "github.com/gofrs/uuid", | ||||
|     "github.com/gorilla/css/scanner", | ||||
|     "github.com/labstack/echo", | ||||
|     "github.com/mafredri/cdp", | ||||
|     "github.com/mafredri/cdp/devtool", | ||||
|   | ||||
| @@ -45,6 +45,10 @@ | ||||
|   name = "github.com/PuerkitoBio/goquery" | ||||
|   version = "1.5.0" | ||||
|  | ||||
| [[constraint]] | ||||
|   name = "github.com/gorilla/css" | ||||
|   version = "v1.0.0" | ||||
|  | ||||
| [[constraint]] | ||||
|   name = "github.com/gofrs/uuid" | ||||
|   version = "3.1.2" | ||||
|   | ||||
| @@ -2,23 +2,52 @@ import random from "../../../utils/random.js"; | ||||
|  | ||||
| const e = React.createElement; | ||||
|  | ||||
| function render(id) { | ||||
|     return  e("span", { id: `${id}-content` }, ["Hello world"]); | ||||
| function render(id, props = {}) { | ||||
|     return  e("span", { id: `${id}-content`, ...props }, ["Hello world"]); | ||||
| } | ||||
|  | ||||
| export default class AppearableComponent extends React.PureComponent { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|  | ||||
|         let element = null; | ||||
|  | ||||
|         if (props.appear) { | ||||
|             if (props.useStyle) { | ||||
|                 element = render(props.id, { style: {display: "none"}}) | ||||
|             } | ||||
|         } else { | ||||
|             if (props.useStyle) { | ||||
|                 element = render(props.id, { style: {display: "block" }}) | ||||
|             } else { | ||||
|                 element = render(props.id) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.state = { | ||||
|             element: props.appear === true ? null : render(props.id) | ||||
|             element | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     handleClick() { | ||||
|         setTimeout(() => { | ||||
|             const props = this.props; | ||||
|             let element = null; | ||||
|  | ||||
|             if (props.appear) { | ||||
|                 if (props.useStyle) { | ||||
|                     element = render(props.id, { style: {display: "block" }}) | ||||
|                 } else { | ||||
|                     element = render(props.id) | ||||
|                 } | ||||
|             } else { | ||||
|                 if (props.useStyle) { | ||||
|                     element = render(props.id, { style: {display: "none"}}) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.setState({ | ||||
|                 element: this.props.appear === true ? render(this.props.id) : null | ||||
|                 element, | ||||
|             }) | ||||
|         }, random()) | ||||
|     } | ||||
|   | ||||
| @@ -48,13 +48,31 @@ export default class EventsPage extends React.Component { | ||||
|                         title: "Appearable" | ||||
|                     }) | ||||
|                 ]), | ||||
|             ]), | ||||
|             e("div", { className: "row" }, [ | ||||
|                 e("div", { className: "col-lg-4"}, [ | ||||
|                     e(Appearable, { | ||||
|                         id: "wait-no-element", | ||||
|                         appear: false, | ||||
|                         title: "Disappearable" | ||||
|                     }) | ||||
|                 ]) | ||||
|                 ]), | ||||
|                 e("div", { className: "col-lg-4"}, [ | ||||
|                     e(Appearable, { | ||||
|                         id: "wait-style", | ||||
|                         appear: true, | ||||
|                         title: "Appearable with style", | ||||
|                         useStyle: true, | ||||
|                     }) | ||||
|                 ]), | ||||
|                 e("div", { className: "col-lg-4"}, [ | ||||
|                     e(Appearable, { | ||||
|                         id: "wait-no-style", | ||||
|                         appear: false, | ||||
|                         title: "Disappearable", | ||||
|                         useStyle: true, | ||||
|                     }) | ||||
|                 ]), | ||||
|             ]) | ||||
|         ]) | ||||
|     } | ||||
|   | ||||
							
								
								
									
										11
									
								
								e2e/tests/el_attrs_get.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								e2e/tests/el_attrs_get.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
|  | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
| LET attrs = ATTR_GET(el, "style") | ||||
|  | ||||
| RETURN EXPECT("display: block;", attrs.style) | ||||
							
								
								
									
										17
									
								
								e2e/tests/el_attrs_remove.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								e2e/tests/el_attrs_remove.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
|  | ||||
| LET prev = el.attributes.style | ||||
|  | ||||
| ATTR_REMOVE(el, "style") | ||||
|  | ||||
| WAIT(1000) | ||||
|  | ||||
| LET curr = el.attributes.style | ||||
|  | ||||
| RETURN prev == "display: block;" && curr == NONE ? "" : "expected attribute to be removed" | ||||
							
								
								
									
										17
									
								
								e2e/tests/el_attrs_set.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								e2e/tests/el_attrs_set.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
|  | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
| LET prev = el.style | ||||
|  | ||||
| ATTR_SET(el, "style", "color: black;") | ||||
|  | ||||
| WAIT(1000) | ||||
|  | ||||
| LET curr = el.style | ||||
|  | ||||
| RETURN curr.color == "black" ? "" : "styles should be updated" | ||||
							
								
								
									
										14
									
								
								e2e/tests/el_attrs_set_bulk.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								e2e/tests/el_attrs_set_bulk.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
|  | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
|  | ||||
| ATTR_SET(el, { style: "color: black;", "data-ferret-x": "test" }) | ||||
|  | ||||
| WAIT(1000) | ||||
|  | ||||
| RETURN el.style.color == "black" && el.attributes["data-ferret-x"] == "test" ? "" : "styles should be updated" | ||||
							
								
								
									
										11
									
								
								e2e/tests/el_style_get.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								e2e/tests/el_style_get.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
|  | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
| LET val = STYLE_GET(el, "display") | ||||
|  | ||||
| RETURN val.display == "block" ? "" : "could not get style values" | ||||
							
								
								
									
										17
									
								
								e2e/tests/el_style_remove.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								e2e/tests/el_style_remove.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
|  | ||||
| LET prev = el.style | ||||
|  | ||||
| STYLE_REMOVE(el, "display") | ||||
|  | ||||
| WAIT(1000) | ||||
|  | ||||
| LET curr = el.style | ||||
|  | ||||
| RETURN prev.display == "block" && curr.display == NONE ? "" : "expected style to be removed" | ||||
							
								
								
									
										17
									
								
								e2e/tests/el_style_set.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								e2e/tests/el_style_set.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
|  | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
| LET prev = el.style | ||||
|  | ||||
| STYLE_SET(el, "color", "black") | ||||
|  | ||||
| WAIT(1000) | ||||
|  | ||||
| LET curr = el.style | ||||
|  | ||||
| RETURN curr.color == "black" ? "" : "styles should be updated" | ||||
							
								
								
									
										17
									
								
								e2e/tests/el_style_set_bulk.d.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								e2e/tests/el_style_set_bulk.d.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| LET url = @dynamic + "?redirect=/events" | ||||
| LET doc = DOCUMENT(url, true) | ||||
| LET pageSelector = "#page-events" | ||||
| LET elemSelector = "#wait-no-style-content" | ||||
|  | ||||
| WAIT_ELEMENT(doc, pageSelector) | ||||
|  | ||||
| LET el = ELEMENT(doc, elemSelector) | ||||
| LET prev = el.style | ||||
|  | ||||
| STYLE_SET(el, { color: "black", "min-width": "100px", "background-color": "#11111" }) | ||||
|  | ||||
| WAIT(1000) | ||||
|  | ||||
| LET curr = el.style | ||||
|  | ||||
| RETURN curr.color == "black" && curr["min-width"] == "100px" && curr["background-color"] == "#11111" ? "" : "styles should be updated" | ||||
| @@ -47,8 +47,8 @@ type ( | ||||
| 		innerHTML      values.String | ||||
| 		innerText      *common.LazyValue | ||||
| 		value          core.Value | ||||
| 		rawAttrs       []string | ||||
| 		attributes     *common.LazyValue | ||||
| 		style          *common.LazyValue | ||||
| 		children       []*HTMLElementIdentity | ||||
| 		loadedChildren *common.LazyValue | ||||
| 	} | ||||
| @@ -128,7 +128,6 @@ func LoadElement( | ||||
| 		id, | ||||
| 		node.Node.NodeType, | ||||
| 		node.Node.NodeName, | ||||
| 		node.Node.Attributes, | ||||
| 		val, | ||||
| 		innerHTML, | ||||
| 		createChildrenArray(node.Node.Children), | ||||
| @@ -142,7 +141,6 @@ func NewHTMLElement( | ||||
| 	id *HTMLElementIdentity, | ||||
| 	nodeType int, | ||||
| 	nodeName string, | ||||
| 	attributes []string, | ||||
| 	value string, | ||||
| 	innerHTML values.String, | ||||
| 	children []*HTMLElementIdentity, | ||||
| @@ -157,8 +155,8 @@ func NewHTMLElement( | ||||
| 	el.nodeName = values.NewString(nodeName) | ||||
| 	el.innerHTML = innerHTML | ||||
| 	el.innerText = common.NewLazyValue(el.loadInnerText) | ||||
| 	el.rawAttrs = attributes | ||||
| 	el.attributes = common.NewLazyValue(el.loadAttrs) | ||||
| 	el.style = common.NewLazyValue(el.parseStyle) | ||||
| 	el.value = values.EmptyString | ||||
| 	el.loadedChildren = common.NewLazyValue(el.loadChildren) | ||||
| 	el.value = values.NewString(value) | ||||
| @@ -297,6 +295,99 @@ func (el *HTMLElement) Length() values.Int { | ||||
| 	return values.NewInt(len(el.children)) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) GetStyles(ctx context.Context) (*values.Object, error) { | ||||
| 	val, err := el.style.Read(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.NewObject(), err | ||||
| 	} | ||||
|  | ||||
| 	if val == values.None { | ||||
| 		return values.NewObject(), nil | ||||
| 	} | ||||
|  | ||||
| 	styles := val.(*values.Object) | ||||
|  | ||||
| 	return styles.Copy().(*values.Object), nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) GetStyle(ctx context.Context, name values.String) (core.Value, error) { | ||||
| 	styles, err := el.style.Read(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	val, found := styles.(*values.Object).Get(name) | ||||
|  | ||||
| 	if !found { | ||||
| 		return values.None, nil | ||||
| 	} | ||||
|  | ||||
| 	return val, nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetStyles(ctx context.Context, styles *values.Object) error { | ||||
| 	if styles == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	val, err := el.style.Read(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	currentStyles := val.(*values.Object) | ||||
|  | ||||
| 	styles.ForEach(func(value core.Value, key string) bool { | ||||
| 		currentStyles.Set(values.NewString(key), value) | ||||
|  | ||||
| 		return true | ||||
| 	}) | ||||
|  | ||||
| 	str := common.SerializeStyles(ctx, currentStyles) | ||||
|  | ||||
| 	return el.SetAttribute(ctx, "style", str) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetStyle(ctx context.Context, name values.String, value core.Value) error { | ||||
| 	val, err := el.style.Read(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	styles := val.(*values.Object) | ||||
| 	styles.Set(name, value) | ||||
|  | ||||
| 	str := common.SerializeStyles(ctx, styles) | ||||
|  | ||||
| 	return el.SetAttribute(ctx, "style", str) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) RemoveStyle(ctx context.Context, names ...values.String) error { | ||||
| 	if len(names) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	val, err := el.style.Read(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	styles := val.(*values.Object) | ||||
|  | ||||
| 	for _, name := range names { | ||||
| 		styles.Remove(name) | ||||
| 	} | ||||
|  | ||||
| 	str := common.SerializeStyles(ctx, styles) | ||||
|  | ||||
| 	return el.SetAttribute(ctx, "style", str) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) GetAttributes(ctx context.Context) *values.Object { | ||||
| 	val, err := el.attributes.Read(ctx) | ||||
|  | ||||
| @@ -326,6 +417,18 @@ func (el *HTMLElement) GetAttribute(ctx context.Context, name values.String) cor | ||||
| 	return val | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetAttributes(ctx context.Context, attrs *values.Object) error { | ||||
| 	var err error | ||||
|  | ||||
| 	attrs.ForEach(func(value core.Value, key string) bool { | ||||
| 		err = el.SetAttribute(ctx, values.NewString(key), values.NewString(value.String())) | ||||
|  | ||||
| 		return err == nil | ||||
| 	}) | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetAttribute(ctx context.Context, name, value values.String) error { | ||||
| 	return el.client.DOM.SetAttributeValue( | ||||
| 		ctx, | ||||
| @@ -333,6 +436,21 @@ func (el *HTMLElement) SetAttribute(ctx context.Context, name, value values.Stri | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) RemoveAttribute(ctx context.Context, names ...values.String) error { | ||||
| 	for _, name := range names { | ||||
| 		err := el.client.DOM.RemoveAttribute( | ||||
| 			ctx, | ||||
| 			dom.NewRemoveAttributeArgs(el.id.nodeID, name.String()), | ||||
| 		) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) GetChildNodes(ctx context.Context) core.Value { | ||||
| 	val, err := el.loadedChildren.Read(ctx) | ||||
|  | ||||
| @@ -940,8 +1058,14 @@ func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) { | ||||
| 	return parsed, nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) loadAttrs(_ context.Context) (core.Value, error) { | ||||
| 	return parseAttrs(el.rawAttrs), nil | ||||
| func (el *HTMLElement) loadAttrs(ctx context.Context) (core.Value, error) { | ||||
| 	repl, err := el.client.DOM.GetAttributes(ctx, dom.NewGetAttributesArgs(el.id.nodeID)) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	return parseAttrs(repl.Attributes), nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) loadChildren(ctx context.Context) (core.Value, error) { | ||||
| @@ -973,6 +1097,20 @@ func (el *HTMLElement) loadChildren(ctx context.Context) (core.Value, error) { | ||||
| 	return loaded, nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) parseStyle(ctx context.Context) (core.Value, error) { | ||||
| 	value := el.GetAttribute(ctx, "style") | ||||
|  | ||||
| 	if value == values.None { | ||||
| 		return values.NewObject(), nil | ||||
| 	} | ||||
|  | ||||
| 	if value.Type() != types.String { | ||||
| 		return values.NewObject(), nil | ||||
| 	} | ||||
|  | ||||
| 	return common.DeserializeStyles(value.(values.String)) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) handlePageReload(_ context.Context, _ interface{}) { | ||||
| 	el.Close() | ||||
| } | ||||
| @@ -990,6 +1128,12 @@ func (el *HTMLElement) handleAttrModified(ctx context.Context, message interface | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// they are not event loaded | ||||
| 	// just ignore the event | ||||
| 	if !el.attributes.Ready() { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	el.attributes.Write(ctx, func(v core.Value, err error) { | ||||
| 		if err != nil { | ||||
| 			el.logError(err).Msg("failed to update element") | ||||
| @@ -997,6 +1141,10 @@ func (el *HTMLElement) handleAttrModified(ctx context.Context, message interface | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if reply.Name == "style" { | ||||
| 			el.style.Reset() | ||||
| 		} | ||||
|  | ||||
| 		attrs, ok := v.(*values.Object) | ||||
|  | ||||
| 		if !ok { | ||||
| @@ -1033,6 +1181,10 @@ func (el *HTMLElement) handleAttrRemoved(ctx context.Context, message interface{ | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if reply.Name == "style" { | ||||
| 			el.style.Reset() | ||||
| 		} | ||||
|  | ||||
| 		attrs, ok := v.(*values.Object) | ||||
|  | ||||
| 		if !ok { | ||||
|   | ||||
| @@ -4,10 +4,10 @@ import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"math" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/cdp/events" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/common" | ||||
|   | ||||
| @@ -59,6 +59,18 @@ func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value | ||||
| 			} | ||||
|  | ||||
| 			return values.GetIn(ctx, attrs, path[1:]) | ||||
| 		case "style": | ||||
| 			styles, err := el.GetStyles(ctx) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return values.None, err | ||||
| 			} | ||||
|  | ||||
| 			if len(path) == 1 { | ||||
| 				return styles, nil | ||||
| 			} | ||||
|  | ||||
| 			return values.GetIn(ctx, styles, path[1:]) | ||||
| 		default: | ||||
| 			return GetInNode(ctx, el, path) | ||||
| 		} | ||||
|   | ||||
| @@ -9,18 +9,18 @@ import ( | ||||
| ) | ||||
|  | ||||
| type ( | ||||
| 	LazyFactory func(ctx context.Context) (core.Value, error) | ||||
| 	LazyValueFactory func(ctx context.Context) (core.Value, error) | ||||
|  | ||||
| 	LazyValue struct { | ||||
| 		sync.Mutex | ||||
| 		factory LazyFactory | ||||
| 		factory LazyValueFactory | ||||
| 		ready   bool | ||||
| 		value   core.Value | ||||
| 		err     error | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func NewLazyValue(factory LazyFactory) *LazyValue { | ||||
| func NewLazyValue(factory LazyValueFactory) *LazyValue { | ||||
| 	lz := new(LazyValue) | ||||
| 	lz.ready = false | ||||
| 	lz.factory = factory | ||||
|   | ||||
| @@ -54,6 +54,15 @@ func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			curr := el.GetAttributes(ctx) | ||||
|  | ||||
| 			// remove all previous attributes | ||||
| 			err = el.RemoveAttribute(ctx, curr.Keys()...) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			obj := value.(*values.Object) | ||||
| 			obj.ForEach(func(value core.Value, key string) bool { | ||||
| 				err = el.SetAttribute(ctx, values.NewString(key), values.NewString(value.String())) | ||||
| @@ -61,6 +70,35 @@ func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value | ||||
| 				return err == nil | ||||
| 			}) | ||||
|  | ||||
| 			return err | ||||
| 		case "style": | ||||
| 			if len(path) > 1 { | ||||
| 				attrName := path[1] | ||||
|  | ||||
| 				return el.SetStyle(ctx, values.NewString(attrName.String()), value) | ||||
| 			} | ||||
|  | ||||
| 			err := core.ValidateType(value, types.Object) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			styles, err := el.GetStyles(ctx) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			err = el.RemoveStyle(ctx, styles.Keys()...) | ||||
|  | ||||
| 			obj := value.(*values.Object) | ||||
| 			obj.ForEach(func(value core.Value, key string) bool { | ||||
| 				err = el.SetStyle(ctx, values.NewString(key), value) | ||||
|  | ||||
| 				return err == nil | ||||
| 			}) | ||||
|  | ||||
| 			return err | ||||
| 		case "value": | ||||
| 			if len(path) > 1 { | ||||
|   | ||||
							
								
								
									
										103
									
								
								pkg/drivers/common/styles.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								pkg/drivers/common/styles.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| package common | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/gorilla/css/scanner" | ||||
| ) | ||||
|  | ||||
| func DeserializeStyles(input values.String) (*values.Object, error) { | ||||
| 	styles := values.NewObject() | ||||
|  | ||||
| 	if input == values.EmptyString { | ||||
| 		return styles, nil | ||||
| 	} | ||||
|  | ||||
| 	s := scanner.New(input.String()) | ||||
|  | ||||
| 	var name string | ||||
| 	var value bytes.Buffer | ||||
| 	var setValue = func() { | ||||
| 		styles.Set(values.NewString(strings.TrimSpace(name)), values.NewString(strings.TrimSpace(value.String()))) | ||||
| 		name = "" | ||||
| 		value.Reset() | ||||
| 	} | ||||
|  | ||||
| 	for { | ||||
| 		token := s.Next() | ||||
|  | ||||
| 		if token == nil { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if token.Type == scanner.TokenEOF { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if name == "" && token.Type == scanner.TokenIdent { | ||||
| 			name = token.Value | ||||
|  | ||||
| 			// skip : and white spaces | ||||
| 			for { | ||||
| 				token = s.Next() | ||||
|  | ||||
| 				if token.Value != ":" && token.Type != scanner.TokenS { | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		switch token.Type { | ||||
| 		case scanner.TokenChar: | ||||
| 			// end of style declaration | ||||
| 			if token.Value == ";" { | ||||
| 				if name != "" { | ||||
| 					setValue() | ||||
| 				} | ||||
| 			} else { | ||||
| 				value.WriteString(token.Value) | ||||
| 			} | ||||
| 		case scanner.TokenNumber: | ||||
| 			num, err := strconv.ParseFloat(token.Value, 64) | ||||
|  | ||||
| 			if err == nil { | ||||
| 				styles.Set(values.NewString(name), values.NewFloat(num)) | ||||
| 				// reset prop | ||||
| 				name = "" | ||||
| 				value.Reset() | ||||
| 			} | ||||
| 		default: | ||||
| 			value.WriteString(token.Value) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if name != "" && value.Len() > 0 { | ||||
| 		setValue() | ||||
| 	} | ||||
|  | ||||
| 	return styles, nil | ||||
| } | ||||
|  | ||||
| func SerializeStyles(_ context.Context, styles *values.Object) values.String { | ||||
| 	if styles == nil { | ||||
| 		return values.EmptyString | ||||
| 	} | ||||
|  | ||||
| 	var b bytes.Buffer | ||||
|  | ||||
| 	styles.ForEach(func(value core.Value, key string) bool { | ||||
| 		b.WriteString(key) | ||||
| 		b.WriteString(": ") | ||||
| 		b.WriteString(value.String()) | ||||
| 		b.WriteString("; ") | ||||
|  | ||||
| 		return true | ||||
| 	}) | ||||
|  | ||||
| 	return values.NewString(b.String()) | ||||
| } | ||||
							
								
								
									
										102
									
								
								pkg/drivers/common/styles_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								pkg/drivers/common/styles_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| package common_test | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/common" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"testing" | ||||
|  | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
|  | ||||
| type style struct { | ||||
| 	raw   string | ||||
| 	name  values.String | ||||
| 	value core.Value | ||||
| } | ||||
|  | ||||
| func TestDeserializeStyles(t *testing.T) { | ||||
| 	Convey("DeserializeStyles", t, func() { | ||||
| 		styles := []style{ | ||||
| 			{ | ||||
| 				raw:   "min-height: 1.15", | ||||
| 				name:  "min-height", | ||||
| 				value: values.NewFloat(1.15), | ||||
| 			}, | ||||
| 			{ | ||||
| 				raw:   "background-color: #4A154B", | ||||
| 				name:  "background-color", | ||||
| 				value: values.NewString("#4A154B"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				raw:   "font-size:26pt", | ||||
| 				name:  "font-size", | ||||
| 				value: values.NewString("26pt"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				raw:   "page-break-after:avoid", | ||||
| 				name:  "page-break-after", | ||||
| 				value: values.NewString("avoid"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				raw:   `font-family: Arial,"Helvetica Neue",Helvetica,sans-serif`, | ||||
| 				name:  "font-family", | ||||
| 				value: values.NewString(`Arial,"Helvetica Neue",Helvetica,sans-serif`), | ||||
| 			}, | ||||
| 			{ | ||||
| 				raw:   "color: black", | ||||
| 				name:  "color", | ||||
| 				value: values.NewString("black"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				raw:   "display: inline-block", | ||||
| 				name:  "display", | ||||
| 				value: values.NewString("inline-block"), | ||||
| 			}, | ||||
| 			{ | ||||
| 				raw:   "min-width: 50", | ||||
| 				name:  "min-width", | ||||
| 				value: values.NewFloat(50), | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		Convey("Should parse a single style", func() { | ||||
| 			for _, s := range styles { | ||||
| 				out, err := common.DeserializeStyles(values.NewString(s.raw)) | ||||
|  | ||||
| 				So(err, ShouldBeNil) | ||||
| 				So(out, ShouldNotBeNil) | ||||
|  | ||||
| 				value, exists := out.Get(s.name) | ||||
|  | ||||
| 				So(bool(exists), ShouldBeTrue) | ||||
|  | ||||
| 				So(value.Compare(s.value) == 0, ShouldBeTrue) | ||||
| 			} | ||||
| 		}) | ||||
|  | ||||
| 		Convey("Should parse multiple styles", func() { | ||||
| 			var buff bytes.Buffer | ||||
|  | ||||
| 			for _, s := range styles { | ||||
| 				buff.WriteString(s.raw) | ||||
| 				buff.WriteString("; ") | ||||
| 			} | ||||
|  | ||||
| 			out, err := common.DeserializeStyles(values.NewString(buff.String())) | ||||
|  | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(out, ShouldNotBeNil) | ||||
| 			So(int(out.Length()), ShouldEqual, len(styles)) | ||||
|  | ||||
| 			for _, s := range styles { | ||||
| 				value, exists := out.Get(s.name) | ||||
|  | ||||
| 				So(bool(exists), ShouldBeTrue) | ||||
|  | ||||
| 				So(value.Compare(s.value) == 0, ShouldBeTrue) | ||||
| 			} | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| @@ -16,6 +16,7 @@ import ( | ||||
| type HTMLElement struct { | ||||
| 	selection *goquery.Selection | ||||
| 	attrs     *values.Object | ||||
| 	styles    *values.Object | ||||
| 	children  *values.Array | ||||
| } | ||||
|  | ||||
| @@ -24,22 +25,22 @@ func NewHTMLElement(node *goquery.Selection) (drivers.HTMLElement, error) { | ||||
| 		return nil, core.Error(core.ErrMissedArgument, "element selection") | ||||
| 	} | ||||
|  | ||||
| 	return &HTMLElement{node, nil, nil}, nil | ||||
| 	return &HTMLElement{node, nil, nil, nil}, nil | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(nd.InnerText(context.Background()).String()) | ||||
| func (el *HTMLElement) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(el.InnerText(context.Background()).String()) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Type() core.Type { | ||||
| func (el *HTMLElement) Type() core.Type { | ||||
| 	return drivers.HTMLElementType | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) String() string { | ||||
| 	return nd.InnerHTML(context.Background()).String() | ||||
| func (el *HTMLElement) String() string { | ||||
| 	return el.InnerHTML(context.Background()).String() | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Compare(other core.Value) int64 { | ||||
| func (el *HTMLElement) Compare(other core.Value) int64 { | ||||
| 	switch other.Type() { | ||||
| 	case drivers.HTMLElementType: | ||||
| 		other := other.(drivers.HTMLElement) | ||||
| @@ -47,18 +48,18 @@ func (nd *HTMLElement) Compare(other core.Value) int64 { | ||||
| 		ctx, fn := drivers.WithDefaultTimeout(context.Background()) | ||||
| 		defer fn() | ||||
|  | ||||
| 		return nd.InnerHTML(ctx).Compare(other.InnerHTML(ctx)) | ||||
| 		return el.InnerHTML(ctx).Compare(other.InnerHTML(ctx)) | ||||
| 	default: | ||||
| 		return drivers.Compare(nd.Type(), other.Type()) | ||||
| 		return drivers.Compare(el.Type(), other.Type()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Unwrap() interface{} { | ||||
| 	return nd.selection | ||||
| func (el *HTMLElement) Unwrap() interface{} { | ||||
| 	return el.selection | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Hash() uint64 { | ||||
| 	str, err := nd.selection.Html() | ||||
| func (el *HTMLElement) Hash() uint64 { | ||||
| 	str, err := el.selection.Html() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| @@ -66,21 +67,21 @@ func (nd *HTMLElement) Hash() uint64 { | ||||
|  | ||||
| 	h := fnv.New64a() | ||||
|  | ||||
| 	h.Write([]byte(nd.Type().String())) | ||||
| 	h.Write([]byte(el.Type().String())) | ||||
| 	h.Write([]byte(":")) | ||||
| 	h.Write([]byte(str)) | ||||
|  | ||||
| 	return h.Sum64() | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Copy() core.Value { | ||||
| 	c, _ := NewHTMLElement(nd.selection.Clone()) | ||||
| func (el *HTMLElement) Copy() core.Value { | ||||
| 	c, _ := NewHTMLElement(el.selection.Clone()) | ||||
|  | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) NodeType() values.Int { | ||||
| 	nodes := nd.selection.Nodes | ||||
| func (el *HTMLElement) NodeType() values.Int { | ||||
| 	nodes := el.selection.Nodes | ||||
|  | ||||
| 	if len(nodes) == 0 { | ||||
| 		return 0 | ||||
| @@ -89,24 +90,24 @@ func (nd *HTMLElement) NodeType() values.Int { | ||||
| 	return values.NewInt(common.ToHTMLType(nodes[0].Type)) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Close() error { | ||||
| func (el *HTMLElement) Close() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) NodeName() values.String { | ||||
| 	return values.NewString(goquery.NodeName(nd.selection)) | ||||
| func (el *HTMLElement) NodeName() values.String { | ||||
| 	return values.NewString(goquery.NodeName(el.selection)) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Length() values.Int { | ||||
| 	if nd.children == nil { | ||||
| 		nd.children = nd.parseChildren() | ||||
| func (el *HTMLElement) Length() values.Int { | ||||
| 	if el.children == nil { | ||||
| 		el.children = el.parseChildren() | ||||
| 	} | ||||
|  | ||||
| 	return nd.children.Length() | ||||
| 	return el.children.Length() | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) GetValue(_ context.Context) core.Value { | ||||
| 	val, ok := nd.selection.Attr("value") | ||||
| func (el *HTMLElement) GetValue(_ context.Context) core.Value { | ||||
| 	val, ok := el.selection.Attr("value") | ||||
|  | ||||
| 	if ok { | ||||
| 		return values.NewString(val) | ||||
| @@ -115,18 +116,18 @@ func (nd *HTMLElement) GetValue(_ context.Context) core.Value { | ||||
| 	return values.EmptyString | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) SetValue(_ context.Context, value core.Value) error { | ||||
| 	nd.selection.SetAttr("value", value.String()) | ||||
| func (el *HTMLElement) SetValue(_ context.Context, value core.Value) error { | ||||
| 	el.selection.SetAttr("value", value.String()) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) InnerText(_ context.Context) values.String { | ||||
| 	return values.NewString(nd.selection.Text()) | ||||
| func (el *HTMLElement) InnerText(_ context.Context) values.String { | ||||
| 	return values.NewString(el.selection.Text()) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) InnerHTML(_ context.Context) values.String { | ||||
| 	h, err := nd.selection.Html() | ||||
| func (el *HTMLElement) InnerHTML(_ context.Context) values.String { | ||||
| 	h, err := el.selection.Html() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.EmptyString | ||||
| @@ -135,48 +136,140 @@ func (nd *HTMLElement) InnerHTML(_ context.Context) values.String { | ||||
| 	return values.NewString(h) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) GetAttributes(_ context.Context) *values.Object { | ||||
| 	if nd.attrs == nil { | ||||
| 		nd.attrs = nd.parseAttrs() | ||||
| func (el *HTMLElement) GetStyles(ctx context.Context) (*values.Object, error) { | ||||
| 	if err := el.ensureStyles(ctx); err != nil { | ||||
| 		return values.NewObject(), err | ||||
| 	} | ||||
|  | ||||
| 	return nd.attrs | ||||
| 	return el.styles.Copy().(*values.Object), nil | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) GetAttribute(_ context.Context, name values.String) core.Value { | ||||
| 	v, ok := nd.selection.Attr(name.String()) | ||||
|  | ||||
| 	if ok { | ||||
| 		return values.NewString(v) | ||||
| func (el *HTMLElement) GetStyle(ctx context.Context, name values.String) (core.Value, error) { | ||||
| 	if err := el.ensureStyles(ctx); err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	return values.None | ||||
| 	return el.styles.MustGet(name), nil | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) SetAttribute(_ context.Context, name, value values.String) error { | ||||
| 	nd.selection.SetAttr(string(name), string(value)) | ||||
| func (el *HTMLElement) SetStyle(ctx context.Context, name values.String, value core.Value) error { | ||||
| 	if err := el.ensureStyles(ctx); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	el.styles.Set(name, value) | ||||
|  | ||||
| 	str := common.SerializeStyles(ctx, el.styles) | ||||
|  | ||||
| 	return el.SetAttribute(ctx, "style", str) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetStyles(ctx context.Context, newStyles *values.Object) error { | ||||
| 	if newStyles == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err := el.ensureStyles(ctx); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	newStyles.ForEach(func(i core.Value, key string) bool { | ||||
| 		el.styles.Set(values.NewString(key), i) | ||||
|  | ||||
| 		return true | ||||
| 	}) | ||||
|  | ||||
| 	str := common.SerializeStyles(ctx, el.styles) | ||||
|  | ||||
| 	return el.SetAttribute(ctx, "style", str) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) RemoveStyle(ctx context.Context, name ...values.String) error { | ||||
| 	if len(name) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err := el.ensureStyles(ctx); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, s := range name { | ||||
| 		el.styles.Remove(s) | ||||
| 	} | ||||
|  | ||||
| 	str := common.SerializeStyles(ctx, el.styles) | ||||
|  | ||||
| 	return el.SetAttribute(ctx, "style", str) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetAttributes(ctx context.Context, attrs *values.Object) error { | ||||
| 	if attrs == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	el.ensureAttrs() | ||||
|  | ||||
| 	var err error | ||||
|  | ||||
| 	attrs.ForEach(func(value core.Value, key string) bool { | ||||
| 		err = el.SetAttribute(ctx, values.NewString(key), values.NewString(value.String())) | ||||
|  | ||||
| 		return err == nil | ||||
| 	}) | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) GetAttributes(_ context.Context) *values.Object { | ||||
| 	el.ensureAttrs() | ||||
|  | ||||
| 	return el.attrs.Copy().(*values.Object) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) GetAttribute(_ context.Context, name values.String) core.Value { | ||||
| 	el.ensureAttrs() | ||||
|  | ||||
| 	return el.attrs.MustGet(name) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetAttribute(_ context.Context, name, value values.String) error { | ||||
| 	el.ensureAttrs() | ||||
|  | ||||
| 	el.attrs.Set(name, value) | ||||
| 	el.selection.SetAttr(string(name), string(value)) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) GetChildNodes(_ context.Context) core.Value { | ||||
| 	if nd.children == nil { | ||||
| 		nd.children = nd.parseChildren() | ||||
| func (el *HTMLElement) RemoveAttribute(_ context.Context, name ...values.String) error { | ||||
| 	el.ensureAttrs() | ||||
|  | ||||
| 	for _, attr := range name { | ||||
| 		el.attrs.Remove(attr) | ||||
| 		el.selection.RemoveAttr(attr.String()) | ||||
| 	} | ||||
|  | ||||
| 	return nd.children | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) GetChildNode(_ context.Context, idx values.Int) core.Value { | ||||
| 	if nd.children == nil { | ||||
| 		nd.children = nd.parseChildren() | ||||
| func (el *HTMLElement) GetChildNodes(_ context.Context) core.Value { | ||||
| 	if el.children == nil { | ||||
| 		el.children = el.parseChildren() | ||||
| 	} | ||||
|  | ||||
| 	return nd.children.Get(idx) | ||||
| 	return el.children | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) QuerySelector(_ context.Context, selector values.String) core.Value { | ||||
| 	selection := nd.selection.Find(selector.String()) | ||||
| func (el *HTMLElement) GetChildNode(_ context.Context, idx values.Int) core.Value { | ||||
| 	if el.children == nil { | ||||
| 		el.children = el.parseChildren() | ||||
| 	} | ||||
|  | ||||
| 	return el.children.Get(idx) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) QuerySelector(_ context.Context, selector values.String) core.Value { | ||||
| 	selection := el.selection.Find(selector.String()) | ||||
|  | ||||
| 	if selection == nil { | ||||
| 		return values.None | ||||
| @@ -191,8 +284,8 @@ func (nd *HTMLElement) QuerySelector(_ context.Context, selector values.String) | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) QuerySelectorAll(_ context.Context, selector values.String) core.Value { | ||||
| 	selection := nd.selection.Find(selector.String()) | ||||
| func (el *HTMLElement) QuerySelectorAll(_ context.Context, selector values.String) core.Value { | ||||
| 	selection := el.selection.Find(selector.String()) | ||||
|  | ||||
| 	if selection == nil { | ||||
| 		return values.None | ||||
| @@ -211,8 +304,8 @@ func (nd *HTMLElement) QuerySelectorAll(_ context.Context, selector values.Strin | ||||
| 	return arr | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) InnerHTMLBySelector(_ context.Context, selector values.String) values.String { | ||||
| 	selection := nd.selection.Find(selector.String()) | ||||
| func (el *HTMLElement) InnerHTMLBySelector(_ context.Context, selector values.String) values.String { | ||||
| 	selection := el.selection.Find(selector.String()) | ||||
|  | ||||
| 	str, err := selection.Html() | ||||
|  | ||||
| @@ -224,8 +317,8 @@ func (nd *HTMLElement) InnerHTMLBySelector(_ context.Context, selector values.St | ||||
| 	return values.NewString(str) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) InnerHTMLBySelectorAll(_ context.Context, selector values.String) *values.Array { | ||||
| 	selection := nd.selection.Find(selector.String()) | ||||
| func (el *HTMLElement) InnerHTMLBySelectorAll(_ context.Context, selector values.String) *values.Array { | ||||
| 	selection := el.selection.Find(selector.String()) | ||||
| 	arr := values.NewArray(selection.Length()) | ||||
|  | ||||
| 	selection.Each(func(_ int, selection *goquery.Selection) { | ||||
| @@ -240,14 +333,14 @@ func (nd *HTMLElement) InnerHTMLBySelectorAll(_ context.Context, selector values | ||||
| 	return arr | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) InnerTextBySelector(_ context.Context, selector values.String) values.String { | ||||
| 	selection := nd.selection.Find(selector.String()) | ||||
| func (el *HTMLElement) InnerTextBySelector(_ context.Context, selector values.String) values.String { | ||||
| 	selection := el.selection.Find(selector.String()) | ||||
|  | ||||
| 	return values.NewString(selection.Text()) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) InnerTextBySelectorAll(_ context.Context, selector values.String) *values.Array { | ||||
| 	selection := nd.selection.Find(selector.String()) | ||||
| func (el *HTMLElement) InnerTextBySelectorAll(_ context.Context, selector values.String) *values.Array { | ||||
| 	selection := el.selection.Find(selector.String()) | ||||
| 	arr := values.NewArray(selection.Length()) | ||||
|  | ||||
| 	selection.Each(func(_ int, selection *goquery.Selection) { | ||||
| @@ -257,8 +350,8 @@ func (nd *HTMLElement) InnerTextBySelectorAll(_ context.Context, selector values | ||||
| 	return arr | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) CountBySelector(_ context.Context, selector values.String) values.Int { | ||||
| 	selection := nd.selection.Find(selector.String()) | ||||
| func (el *HTMLElement) CountBySelector(_ context.Context, selector values.String) values.Int { | ||||
| 	selection := el.selection.Find(selector.String()) | ||||
|  | ||||
| 	if selection == nil { | ||||
| 		return values.ZeroInt | ||||
| @@ -267,8 +360,8 @@ func (nd *HTMLElement) CountBySelector(_ context.Context, selector values.String | ||||
| 	return values.NewInt(selection.Size()) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) ExistsBySelector(_ context.Context, selector values.String) values.Boolean { | ||||
| 	selection := nd.selection.Closest(selector.String()) | ||||
| func (el *HTMLElement) ExistsBySelector(_ context.Context, selector values.String) values.Boolean { | ||||
| 	selection := el.selection.Closest(selector.String()) | ||||
|  | ||||
| 	if selection == nil { | ||||
| 		return values.False | ||||
| @@ -277,47 +370,83 @@ func (nd *HTMLElement) ExistsBySelector(_ context.Context, selector values.Strin | ||||
| 	return values.True | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) GetIn(ctx context.Context, path []core.Value) (core.Value, error) { | ||||
| 	return common.GetInElement(ctx, nd, path) | ||||
| func (el *HTMLElement) GetIn(ctx context.Context, path []core.Value) (core.Value, error) { | ||||
| 	return common.GetInElement(ctx, el, path) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core.Value) error { | ||||
| 	return common.SetInElement(ctx, nd, path, value) | ||||
| func (el *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core.Value) error { | ||||
| 	return common.SetInElement(ctx, el, path, value) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Iterate(_ context.Context) (core.Iterator, error) { | ||||
| 	return common.NewIterator(nd) | ||||
| func (el *HTMLElement) Iterate(_ context.Context) (core.Iterator, error) { | ||||
| 	return common.NewIterator(el) | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Click(_ context.Context) (values.Boolean, error) { | ||||
| func (el *HTMLElement) Click(_ context.Context) (values.Boolean, error) { | ||||
| 	return false, core.ErrNotSupported | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Input(_ context.Context, _ core.Value, _ values.Int) error { | ||||
| func (el *HTMLElement) Input(_ context.Context, _ core.Value, _ values.Int) error { | ||||
| 	return core.ErrNotSupported | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Select(_ context.Context, _ *values.Array) (*values.Array, error) { | ||||
| func (el *HTMLElement) Select(_ context.Context, _ *values.Array) (*values.Array, error) { | ||||
| 	return nil, core.ErrNotSupported | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) ScrollIntoView(_ context.Context) error { | ||||
| func (el *HTMLElement) ScrollIntoView(_ context.Context) error { | ||||
| 	return core.ErrNotSupported | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) Hover(_ context.Context) error { | ||||
| func (el *HTMLElement) Hover(_ context.Context) error { | ||||
| 	return core.ErrNotSupported | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) WaitForClass(_ context.Context, _ values.String, _ drivers.WaitEvent) error { | ||||
| func (el *HTMLElement) WaitForClass(_ context.Context, _ values.String, _ drivers.WaitEvent) error { | ||||
| 	return core.ErrNotSupported | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) parseAttrs() *values.Object { | ||||
| func (el *HTMLElement) ensureStyles(ctx context.Context) error { | ||||
| 	if el.styles == nil { | ||||
| 		styles, err := el.parseStyles(ctx) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		el.styles = styles | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) parseStyles(ctx context.Context) (*values.Object, error) { | ||||
| 	str := el.GetAttribute(ctx, "style") | ||||
|  | ||||
| 	if str == values.None { | ||||
| 		return values.NewObject(), nil | ||||
| 	} | ||||
|  | ||||
| 	styles, err := common.DeserializeStyles(values.NewString(str.String())) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return styles, nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) ensureAttrs() { | ||||
| 	if el.attrs == nil { | ||||
| 		el.attrs = el.parseAttrs() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) parseAttrs() *values.Object { | ||||
| 	obj := values.NewObject() | ||||
|  | ||||
| 	for _, name := range common.Attributes { | ||||
| 		val, ok := nd.selection.Attr(name) | ||||
| 		val, ok := el.selection.Attr(name) | ||||
|  | ||||
| 		if ok { | ||||
| 			obj.Set(values.NewString(name), values.NewString(val)) | ||||
| @@ -327,8 +456,8 @@ func (nd *HTMLElement) parseAttrs() *values.Object { | ||||
| 	return obj | ||||
| } | ||||
|  | ||||
| func (nd *HTMLElement) parseChildren() *values.Array { | ||||
| 	children := nd.selection.Children() | ||||
| func (el *HTMLElement) parseChildren() *values.Array { | ||||
| 	children := el.selection.Children() | ||||
|  | ||||
| 	arr := values.NewArray(10) | ||||
|  | ||||
|   | ||||
| @@ -53,12 +53,26 @@ type ( | ||||
|  | ||||
| 		SetValue(ctx context.Context, value core.Value) error | ||||
|  | ||||
| 		GetStyles(ctx context.Context) (*values.Object, error) | ||||
|  | ||||
| 		GetStyle(ctx context.Context, name values.String) (core.Value, error) | ||||
|  | ||||
| 		SetStyles(ctx context.Context, values *values.Object) error | ||||
|  | ||||
| 		SetStyle(ctx context.Context, name values.String, value core.Value) error | ||||
|  | ||||
| 		RemoveStyle(ctx context.Context, name ...values.String) error | ||||
|  | ||||
| 		GetAttributes(ctx context.Context) *values.Object | ||||
|  | ||||
| 		GetAttribute(ctx context.Context, name values.String) core.Value | ||||
|  | ||||
| 		SetAttributes(ctx context.Context, values *values.Object) error | ||||
|  | ||||
| 		SetAttribute(ctx context.Context, name, value values.String) error | ||||
|  | ||||
| 		RemoveAttribute(ctx context.Context, name ...values.String) error | ||||
|  | ||||
| 		InnerHTMLBySelector(ctx context.Context, selector values.String) values.String | ||||
|  | ||||
| 		InnerHTMLBySelectorAll(ctx context.Context, selector values.String) *values.Array | ||||
|   | ||||
| @@ -22,7 +22,7 @@ type ( | ||||
| 	KeyedCollection interface { | ||||
| 		core.Value | ||||
| 		Measurable | ||||
| 		Keys() []string | ||||
| 		Keys() []values.String | ||||
| 		Get(key values.String) (core.Value, values.Boolean) | ||||
| 		Set(key values.String, value core.Value) | ||||
| 	} | ||||
|   | ||||
| @@ -10,7 +10,7 @@ type KeyedIterator struct { | ||||
| 	valVar string | ||||
| 	keyVar string | ||||
| 	values KeyedCollection | ||||
| 	keys   []string | ||||
| 	keys   []values.String | ||||
| 	pos    int | ||||
| } | ||||
|  | ||||
| @@ -40,7 +40,7 @@ func (iterator *KeyedIterator) Next(_ context.Context, scope *core.Scope) (*core | ||||
| 	} | ||||
|  | ||||
| 	if len(iterator.keys) > iterator.pos { | ||||
| 		key := values.NewString(iterator.keys[iterator.pos]) | ||||
| 		key := iterator.keys[iterator.pos] | ||||
| 		val, _ := iterator.values.Get(key) | ||||
|  | ||||
| 		iterator.pos++ | ||||
|   | ||||
| @@ -80,10 +80,23 @@ func (t *Object) Compare(other core.Value) int64 { | ||||
|  | ||||
| 		var res int64 | ||||
|  | ||||
| 		sortedT := sort.StringSlice(t.Keys()) | ||||
| 		tKeys := make([]string, 0, len(t.value)) | ||||
|  | ||||
| 		for k := range t.value { | ||||
| 			tKeys = append(tKeys, k) | ||||
| 		} | ||||
|  | ||||
| 		sortedT := sort.StringSlice(tKeys) | ||||
| 		sortedT.Sort() | ||||
|  | ||||
| 		sortedOther := sort.StringSlice(other.Keys()) | ||||
| 		otherKeys := make([]string, 0, other.Length()) | ||||
|  | ||||
| 		other.ForEach(func(value core.Value, k string) bool { | ||||
| 			otherKeys = append(otherKeys, k) | ||||
| 			return true | ||||
| 		}) | ||||
|  | ||||
| 		sortedOther := sort.StringSlice(otherKeys) | ||||
| 		sortedOther.Sort() | ||||
|  | ||||
| 		var tVal, otherVal core.Value | ||||
| @@ -178,11 +191,21 @@ func (t *Object) Length() Int { | ||||
| 	return Int(len(t.value)) | ||||
| } | ||||
|  | ||||
| func (t *Object) Keys() []string { | ||||
| 	keys := make([]string, 0, len(t.value)) | ||||
| func (t *Object) Keys() []String { | ||||
| 	keys := make([]String, 0, len(t.value)) | ||||
|  | ||||
| 	for k := range t.value { | ||||
| 		keys = append(keys, k) | ||||
| 		keys = append(keys, NewString(k)) | ||||
| 	} | ||||
|  | ||||
| 	return keys | ||||
| } | ||||
|  | ||||
| func (t *Object) Values() []core.Value { | ||||
| 	keys := make([]core.Value, 0, len(t.value)) | ||||
|  | ||||
| 	for _, v := range t.value { | ||||
| 		keys = append(keys, v) | ||||
| 	} | ||||
|  | ||||
| 	return keys | ||||
|   | ||||
| @@ -21,7 +21,6 @@ func NewLib() map[string]core.Function { | ||||
| 		"REMOVE_NTH":     RemoveNth, | ||||
| 		"REMOVE_VALUE":   RemoveValue, | ||||
| 		"REMOVE_VALUES":  RemoveValues, | ||||
| 		"REVERSE":        Reverse, | ||||
| 		"SHIFT":          Shift, | ||||
| 		"SLICE":          Slice, | ||||
| 		"SORTED":         Sorted, | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| package arrays | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // Reverse return a new array with its elements reversed. | ||||
| // @param array (Array) - Target array. | ||||
| // @returns (Array) - A new array with its elements reversed. | ||||
| func Reverse(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 1, 1) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], types.Array) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	arr := args[0].(*values.Array) | ||||
| 	size := int(arr.Length()) | ||||
| 	result := values.NewArray(size) | ||||
|  | ||||
| 	for i := size - 1; i >= 0; i-- { | ||||
| 		result.Push(arr.Get(values.NewInt(i))) | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
| @@ -4,6 +4,7 @@ import "github.com/MontFerret/ferret/pkg/runtime/core" | ||||
|  | ||||
| func NewLib() map[string]core.Function { | ||||
| 	return map[string]core.Function{ | ||||
| 		"LENGTH": Length, | ||||
| 		"LENGTH":  Length, | ||||
| 		"REVERSE": Reverse, | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										51
									
								
								pkg/stdlib/collections/reverse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								pkg/stdlib/collections/reverse.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| package collections | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
|  | ||||
| // Reverse returns the reverse of a given string or array value. | ||||
| // @param text (String|Array) - The string or array to reverse. | ||||
| // @returns (String|Array) - Returns a reversed version of a given value. | ||||
| func Reverse(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 1, 1) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.EmptyString, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], types.Array, types.String) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	switch col := args[0].(type) { | ||||
| 	case values.String: | ||||
| 		runes := []rune(string(col)) | ||||
| 		size := len(runes) | ||||
|  | ||||
| 		// Reverse | ||||
| 		for i := 0; i < size/2; i++ { | ||||
| 			runes[i], runes[size-1-i] = runes[size-1-i], runes[i] | ||||
| 		} | ||||
|  | ||||
| 		return values.NewString(string(runes)), nil | ||||
| 	case *values.Array: | ||||
| 		size := int(col.Length()) | ||||
| 		result := values.NewArray(size) | ||||
|  | ||||
| 		for i := size - 1; i >= 0; i-- { | ||||
| 			result.Push(col.Get(values.NewInt(i))) | ||||
| 		} | ||||
|  | ||||
| 		return result, nil | ||||
|  | ||||
| 	default: | ||||
| 		return values.None, nil | ||||
| 	} | ||||
| } | ||||
| @@ -1,14 +1,33 @@ | ||||
| package arrays_test | ||||
| package collections_test | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/stdlib/arrays" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/stdlib/collections" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
| func TestReverse(t *testing.T) { | ||||
| 	Convey("When args are not passed", t, func() { | ||||
| 		Convey("It should return an error", func() { | ||||
| 			var err error | ||||
| 			_, err = collections.Reverse(context.Background()) | ||||
| 
 | ||||
| 			So(err, ShouldBeError) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	Convey("Should reverse a text with right encoding", t, func() { | ||||
| 		out, _ := collections.Reverse( | ||||
| 			context.Background(), | ||||
| 			values.NewString("The quick brown 狐 jumped over the lazy 犬"), | ||||
| 		) | ||||
| 
 | ||||
| 		So(out, ShouldEqual, "犬 yzal eht revo depmuj 狐 nworb kciuq ehT") | ||||
| 	}) | ||||
| 
 | ||||
| 	Convey("Should return a copy of an array with reversed elements", t, func() { | ||||
| 		arr := values.NewArrayWith( | ||||
| 			values.NewInt(1), | ||||
| @@ -19,7 +38,7 @@ func TestReverse(t *testing.T) { | ||||
| 			values.NewInt(6), | ||||
| 		) | ||||
| 
 | ||||
| 		out, err := arrays.Reverse( | ||||
| 		out, err := collections.Reverse( | ||||
| 			context.Background(), | ||||
| 			arr, | ||||
| 		) | ||||
| @@ -31,7 +50,7 @@ func TestReverse(t *testing.T) { | ||||
| 	Convey("Should return an empty array when there no elements in a source one", t, func() { | ||||
| 		arr := values.NewArray(0) | ||||
| 
 | ||||
| 		out, err := arrays.Reverse( | ||||
| 		out, err := collections.Reverse( | ||||
| 			context.Background(), | ||||
| 			arr, | ||||
| 		) | ||||
							
								
								
									
										41
									
								
								pkg/stdlib/html/attr_get.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								pkg/stdlib/html/attr_get.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
|  | ||||
| // AttributeGet gets single or more attribute(s) of a given element. | ||||
| // @param el (HTMLElement) - Target element. | ||||
| // @param names (...String) - Attribute name(s). | ||||
| // @returns Object - Key-value pairs of attribute values. | ||||
| func AttributeGet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	names := args[1:] | ||||
| 	result := values.NewObject() | ||||
| 	attrs := el.GetAttributes(ctx) | ||||
|  | ||||
| 	for _, n := range names { | ||||
| 		name := values.NewString(n.String()) | ||||
| 		val, exists := attrs.Get(name) | ||||
|  | ||||
| 		if exists { | ||||
| 			result.Set(name, val) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
							
								
								
									
										41
									
								
								pkg/stdlib/html/attr_remove.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								pkg/stdlib/html/attr_remove.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // AttributeRemove removes single or more attribute(s) of a given element. | ||||
| // @param el (HTMLElement) - Target element. | ||||
| // @param names (...String) - Attribute name(s). | ||||
| func AttributeRemove(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	attrs := args[1:] | ||||
| 	attrsStr := make([]values.String, 0, len(attrs)) | ||||
|  | ||||
| 	for _, attr := range attrs { | ||||
| 		str, ok := attr.(values.String) | ||||
|  | ||||
| 		if !ok { | ||||
| 			return values.None, core.TypeError(attr.Type(), types.String) | ||||
| 		} | ||||
|  | ||||
| 		attrsStr = append(attrsStr, str) | ||||
| 	} | ||||
|  | ||||
| 	return values.None, el.RemoveAttribute(ctx, attrsStr...) | ||||
| } | ||||
							
								
								
									
										50
									
								
								pkg/stdlib/html/attr_set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								pkg/stdlib/html/attr_set.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // AttributeSet sets or updates a single or more attribute(s) of a given element. | ||||
| // @param el (HTMLElement) - Target element. | ||||
| // @param nameOrObj (String | Object) - Attribute name or an object representing a key-value pair of attributes. | ||||
| // @param value (String) - If a second parameter is a string value, this parameter represent an attribute value. | ||||
| func AttributeSet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	switch arg1 := args[1].(type) { | ||||
| 	case values.String: | ||||
| 		// ATTR_SET(el, name, value) | ||||
| 		err = core.ValidateArgs(args, 3, 3) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, nil | ||||
| 		} | ||||
|  | ||||
| 		arg2, ok := args[2].(values.String) | ||||
|  | ||||
| 		if !ok { | ||||
| 			return values.None, core.TypeError(arg1.Type(), types.String, types.Object) | ||||
| 		} | ||||
|  | ||||
| 		return values.None, el.SetAttribute(ctx, arg1, arg2) | ||||
| 	case *values.Object: | ||||
| 		// ATTR_SET(el, values) | ||||
| 		return values.None, el.SetAttributes(ctx, arg1) | ||||
| 	default: | ||||
| 		return values.None, core.TypeError(arg1.Type(), types.String, types.Object) | ||||
| 	} | ||||
| } | ||||
| @@ -14,6 +14,9 @@ const defaultTimeout = 5000 | ||||
|  | ||||
| func NewLib() map[string]core.Function { | ||||
| 	return map[string]core.Function{ | ||||
| 		"ATTR_GET":          AttributeGet, | ||||
| 		"ATTR_REMOVE":       AttributeRemove, | ||||
| 		"ATTR_SET":          AttributeSet, | ||||
| 		"CLICK":             Click, | ||||
| 		"CLICK_ALL":         ClickAll, | ||||
| 		"DOCUMENT":          Document, | ||||
| @@ -40,6 +43,9 @@ func NewLib() map[string]core.Function { | ||||
| 		"SCROLL_ELEMENT":    ScrollInto, | ||||
| 		"SCROLL_TOP":        ScrollTop, | ||||
| 		"SELECT":            Select, | ||||
| 		"STYLE_GET":         StyleGet, | ||||
| 		"STYLE_REMOVE":      StyleRemove, | ||||
| 		"STYLE_SET":         StyleSet, | ||||
| 		"WAIT_ELEMENT":      WaitElement, | ||||
| 		"WAIT_NO_ELEMENT":   WaitNoElement, | ||||
| 		"WAIT_CLASS":        WaitClass, | ||||
|   | ||||
							
								
								
									
										44
									
								
								pkg/stdlib/html/style_get.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								pkg/stdlib/html/style_get.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
|  | ||||
| // StyleGet gets single or more style attribute value(s) of a given element. | ||||
| // @param el (HTMLElement) - Target element. | ||||
| // @param names (...String) - Style name(s). | ||||
| // @returns Object - Key-value pairs of style values. | ||||
| func StyleGet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	names := args[1:] | ||||
| 	result := values.NewObject() | ||||
|  | ||||
| 	for _, n := range names { | ||||
| 		name := values.NewString(n.String()) | ||||
| 		val, err := el.GetStyle(ctx, name) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, err | ||||
| 		} | ||||
|  | ||||
| 		if val != values.None { | ||||
| 			result.Set(name, val) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
							
								
								
									
										41
									
								
								pkg/stdlib/html/style_remove.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								pkg/stdlib/html/style_remove.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // StyleRemove removes single or more style attribute value(s) of a given element. | ||||
| // @param el (HTMLElement) - Target element. | ||||
| // @param names (...String) - Style name(s). | ||||
| func StyleRemove(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	attrs := args[1:] | ||||
| 	attrsStr := make([]values.String, 0, len(attrs)) | ||||
|  | ||||
| 	for _, attr := range attrs { | ||||
| 		str, ok := attr.(values.String) | ||||
|  | ||||
| 		if !ok { | ||||
| 			return values.None, core.TypeError(attr.Type(), types.String) | ||||
| 		} | ||||
|  | ||||
| 		attrsStr = append(attrsStr, str) | ||||
| 	} | ||||
|  | ||||
| 	return values.None, el.RemoveStyle(ctx, attrsStr...) | ||||
| } | ||||
							
								
								
									
										44
									
								
								pkg/stdlib/html/style_set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								pkg/stdlib/html/style_set.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // StyleSet sets or updates a single or more style attribute value of a given element. | ||||
| // @param el (HTMLElement) - Target element. | ||||
| // @param nameOrObj (String | Object) - Style name or an object representing a key-value pair of attributes. | ||||
| // @param value (String) - If a second parameter is a string value, this parameter represent a style value. | ||||
| func StyleSet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	switch arg1 := args[1].(type) { | ||||
| 	case values.String: | ||||
| 		// STYLE_SET(el, name, value) | ||||
| 		err = core.ValidateArgs(args, 3, 3) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, nil | ||||
| 		} | ||||
|  | ||||
| 		return values.None, el.SetStyle(ctx, arg1, args[2]) | ||||
| 	case *values.Object: | ||||
| 		// STYLE_SET(el, values) | ||||
| 		return values.None, el.SetStyles(ctx, arg1) | ||||
| 	default: | ||||
| 		return values.None, core.TypeError(arg1.Type(), types.String, types.Object) | ||||
| 	} | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| package stdlib | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/stdlib/arrays" | ||||
| 	"github.com/MontFerret/ferret/pkg/stdlib/collections" | ||||
| @@ -17,6 +18,10 @@ func NewLib() map[string]core.Function { | ||||
|  | ||||
| 	add := func(l map[string]core.Function) { | ||||
| 		for name, fn := range l { | ||||
| 			if _, exists := lib[name]; exists { | ||||
| 				panic(fmt.Sprintf("%s function already exists", name)) | ||||
| 			} | ||||
|  | ||||
| 			lib[name] = fn | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -185,15 +185,15 @@ func isEqualObjects(obj1 *values.Object, obj2 *values.Object) bool { | ||||
| 	var val2 core.Value | ||||
|  | ||||
| 	for _, key := range obj1.Keys() { | ||||
| 		val1, _ = obj1.Get(values.NewString(key)) | ||||
| 		val2, _ = obj2.Get(values.NewString(key)) | ||||
| 		val1, _ = obj1.Get(key) | ||||
| 		val2, _ = obj2.Get(key) | ||||
| 		if val1.Compare(val2) != 0 { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	for _, key := range obj2.Keys() { | ||||
| 		val1, _ = obj1.Get(values.NewString(key)) | ||||
| 		val2, _ = obj2.Get(values.NewString(key)) | ||||
| 		val1, _ = obj1.Get(key) | ||||
| 		val2, _ = obj2.Get(key) | ||||
| 		if val2.Compare(val1) != 0 { | ||||
| 			return false | ||||
| 		} | ||||
|   | ||||
| @@ -36,7 +36,15 @@ func Keys(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		needSort = bool(args[1].(values.Boolean)) | ||||
| 	} | ||||
|  | ||||
| 	keys := sort.StringSlice(obj.Keys()) | ||||
| 	oKeys := make([]string, 0, obj.Length()) | ||||
|  | ||||
| 	obj.ForEach(func(value core.Value, key string) bool { | ||||
| 		oKeys = append(oKeys, key) | ||||
|  | ||||
| 		return true | ||||
| 	}) | ||||
|  | ||||
| 	keys := sort.StringSlice(oKeys) | ||||
| 	keysArray := values.NewArray(len(keys)) | ||||
|  | ||||
| 	if needSort { | ||||
|   | ||||
| @@ -22,7 +22,6 @@ func NewLib() map[string]core.Function { | ||||
| 		"REGEXP_SPLIT":         RegexSplit, | ||||
| 		"REGEXP_TEST":          RegexTest, | ||||
| 		"REGEXP_REPLACE":       RegexReplace, | ||||
| 		"REVERSE":              Reverse, | ||||
| 		"RIGHT":                Right, | ||||
| 		"RTRIM":                RTrim, | ||||
| 		"SHA1":                 Sha1, | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| package strings | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
|  | ||||
| // Reverse returns the reverse of the string value. | ||||
| // @param text (String) - The string to revers | ||||
| // @returns (String) - Returns a reversed version of the string. | ||||
| func Reverse(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 1, 1) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.EmptyString, err | ||||
| 	} | ||||
|  | ||||
| 	text := args[0].String() | ||||
| 	runes := []rune(text) | ||||
| 	size := len(runes) | ||||
|  | ||||
| 	// Reverse | ||||
| 	for i := 0; i < size/2; i++ { | ||||
| 		runes[i], runes[size-1-i] = runes[size-1-i], runes[i] | ||||
| 	} | ||||
|  | ||||
| 	return values.NewString(string(runes)), nil | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| package strings_test | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/stdlib/strings" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestReverse(t *testing.T) { | ||||
| 	Convey("When args are not passed", t, func() { | ||||
| 		Convey("It should return an error", func() { | ||||
| 			var err error | ||||
| 			_, err = strings.Reverse(context.Background()) | ||||
|  | ||||
| 			So(err, ShouldBeError) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	Convey("Should reverse a text with right encoding", t, func() { | ||||
| 		out, _ := strings.Reverse( | ||||
| 			context.Background(), | ||||
| 			values.NewString("The quick brown 狐 jumped over the lazy 犬"), | ||||
| 		) | ||||
|  | ||||
| 		So(out, ShouldEqual, "犬 yzal eht revo depmuj 狐 nworb kciuq ehT") | ||||
| 	}) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user