2018-09-18 22:42:38 +02:00
|
|
|
package browser
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/sha512"
|
|
|
|
"encoding/json"
|
|
|
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
|
|
|
"github.com/MontFerret/ferret/pkg/runtime/values"
|
2018-09-25 23:58:57 +02:00
|
|
|
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/browser/events"
|
2018-09-18 22:42:38 +02:00
|
|
|
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/common"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
|
|
"github.com/mafredri/cdp"
|
|
|
|
"github.com/mafredri/cdp/protocol/dom"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const DefaultTimeout = time.Second * 30
|
|
|
|
|
|
|
|
type HtmlElement struct {
|
2018-09-25 17:43:58 +02:00
|
|
|
client *cdp.Client
|
|
|
|
id dom.NodeID
|
|
|
|
nodeType values.Int
|
|
|
|
nodeName values.String
|
|
|
|
value string
|
|
|
|
attributes *values.Object
|
|
|
|
children []dom.NodeID
|
|
|
|
loadedChildren *values.Array
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
func LoadElement(
|
2018-09-18 22:42:38 +02:00
|
|
|
client *cdp.Client,
|
|
|
|
id dom.NodeID,
|
|
|
|
) (*HtmlElement, error) {
|
|
|
|
if client == nil {
|
|
|
|
return nil, core.Error(core.ErrMissedArgument, "client")
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
|
|
|
|
|
|
|
defer cancelFn()
|
|
|
|
|
|
|
|
node, err := client.DOM.DescribeNode(
|
|
|
|
ctx,
|
|
|
|
dom.
|
|
|
|
NewDescribeNodeArgs().
|
|
|
|
SetNodeID(id).
|
2018-09-25 17:43:58 +02:00
|
|
|
SetDepth(1),
|
2018-09-18 22:42:38 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, core.Error(err, strconv.Itoa(int(id)))
|
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
return NewHtmlElement(client, id, node.Node), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewHtmlElement(
|
|
|
|
client *cdp.Client,
|
|
|
|
id dom.NodeID,
|
|
|
|
node dom.Node,
|
|
|
|
) *HtmlElement {
|
|
|
|
el := new(HtmlElement)
|
|
|
|
el.client = client
|
|
|
|
el.id = id
|
|
|
|
el.nodeType = values.NewInt(node.NodeType)
|
|
|
|
el.nodeName = values.NewString(node.NodeName)
|
|
|
|
el.value = ""
|
|
|
|
el.attributes = parseAttrs(node.Attributes)
|
|
|
|
|
|
|
|
var childCount int
|
|
|
|
|
|
|
|
if node.ChildNodeCount != nil {
|
|
|
|
childCount = *node.ChildNodeCount
|
|
|
|
}
|
|
|
|
|
|
|
|
if node.Value != nil {
|
|
|
|
el.value = *node.Value
|
|
|
|
}
|
|
|
|
|
|
|
|
el.children = make([]dom.NodeID, childCount)
|
|
|
|
|
|
|
|
for idx, child := range node.Children {
|
|
|
|
el.children[idx] = child.NodeID
|
|
|
|
}
|
|
|
|
|
|
|
|
return el
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) Close() error {
|
|
|
|
// el.client = nil
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) Type() core.Type {
|
|
|
|
return core.HtmlElementType
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) MarshalJSON() ([]byte, error) {
|
|
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
|
|
|
|
|
|
|
defer cancelFn()
|
|
|
|
|
|
|
|
args := dom.NewGetOuterHTMLArgs()
|
2018-09-25 17:43:58 +02:00
|
|
|
args.NodeID = &el.id
|
2018-09-18 22:42:38 +02:00
|
|
|
|
|
|
|
reply, err := el.client.DOM.GetOuterHTML(ctx, args)
|
|
|
|
|
|
|
|
if err != nil {
|
2018-09-25 17:43:58 +02:00
|
|
|
return nil, core.Error(err, strconv.Itoa(int(el.id)))
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return json.Marshal(reply.OuterHTML)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) String() string {
|
2018-09-25 17:43:58 +02:00
|
|
|
return el.value
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) Compare(other core.Value) int {
|
|
|
|
switch other.Type() {
|
|
|
|
case core.HtmlDocumentType:
|
|
|
|
other := other.(*HtmlElement)
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
id := int(el.id)
|
|
|
|
otherId := int(other.id)
|
2018-09-18 22:42:38 +02:00
|
|
|
|
|
|
|
if id == otherId {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
if id > otherId {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1
|
|
|
|
default:
|
|
|
|
if other.Type() > core.HtmlElementType {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) Unwrap() interface{} {
|
2018-09-25 17:43:58 +02:00
|
|
|
return el
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) Hash() int {
|
|
|
|
h := sha512.New()
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
out, err := h.Write([]byte(el.value))
|
2018-09-18 22:42:38 +02:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) Value() core.Value {
|
|
|
|
return values.None
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) Length() values.Int {
|
2018-09-25 17:43:58 +02:00
|
|
|
return values.NewInt(len(el.children))
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) NodeType() values.Int {
|
2018-09-25 17:43:58 +02:00
|
|
|
return el.nodeType
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) NodeName() values.String {
|
2018-09-25 17:43:58 +02:00
|
|
|
return el.nodeName
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) GetAttributes() core.Value {
|
|
|
|
return el.attributes
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) GetAttribute(name values.String) core.Value {
|
|
|
|
val, found := el.attributes.Get(name)
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
return values.None
|
|
|
|
}
|
|
|
|
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) GetChildNodes() core.Value {
|
2018-09-25 17:43:58 +02:00
|
|
|
if el.loadedChildren == nil {
|
|
|
|
el.loadedChildren = loadNodes(el.client, el.children)
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
return el.loadedChildren
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) GetChildNode(idx values.Int) core.Value {
|
2018-09-25 17:43:58 +02:00
|
|
|
if el.loadedChildren == nil {
|
|
|
|
el.loadedChildren = loadNodes(el.client, el.children)
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
return el.loadedChildren.Get(idx)
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) QuerySelector(selector values.String) core.Value {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
selectorArgs := dom.NewQuerySelectorArgs(el.id, selector.String())
|
|
|
|
found, err := el.client.DOM.QuerySelector(ctx, selectorArgs)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return values.None
|
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
res, err := LoadElement(el.client, found.NodeID)
|
2018-09-18 22:42:38 +02:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return values.None
|
|
|
|
}
|
|
|
|
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) QuerySelectorAll(selector values.String) core.Value {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
selectorArgs := dom.NewQuerySelectorAllArgs(el.id, selector.String())
|
|
|
|
res, err := el.client.DOM.QuerySelectorAll(ctx, selectorArgs)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return values.None
|
|
|
|
}
|
|
|
|
|
|
|
|
arr := values.NewArray(len(res.NodeIDs))
|
|
|
|
|
|
|
|
for _, id := range res.NodeIDs {
|
2018-09-25 17:43:58 +02:00
|
|
|
childEl, err := LoadElement(el.client, id)
|
2018-09-18 22:42:38 +02:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return values.None
|
|
|
|
}
|
|
|
|
|
|
|
|
arr.Push(childEl)
|
|
|
|
}
|
|
|
|
|
|
|
|
return arr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) InnerText() values.String {
|
|
|
|
h := el.InnerHtml()
|
|
|
|
|
|
|
|
if h == values.EmptyString {
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
buff := bytes.NewBuffer([]byte(h))
|
|
|
|
|
|
|
|
parsed, err := goquery.NewDocumentFromReader(buff)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return values.EmptyString
|
|
|
|
}
|
|
|
|
|
|
|
|
return values.NewString(parsed.Text())
|
|
|
|
}
|
|
|
|
|
|
|
|
func (el *HtmlElement) InnerHtml() values.String {
|
2018-09-25 17:43:58 +02:00
|
|
|
ctx, cancelFn := createCtx()
|
2018-09-18 22:42:38 +02:00
|
|
|
|
|
|
|
defer cancelFn()
|
|
|
|
|
|
|
|
res, err := el.client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetNodeID(el.id))
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return values.EmptyString
|
|
|
|
}
|
|
|
|
|
|
|
|
return values.NewString(res.OuterHTML)
|
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
func (el *HtmlElement) Click() (values.Boolean, error) {
|
|
|
|
ctx, cancel := createCtx()
|
|
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
2018-09-25 23:58:57 +02:00
|
|
|
return events.DispatchEvent(ctx, el.client, el.id, "click")
|
2018-09-25 17:43:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func createCtx() (context.Context, context.CancelFunc) {
|
2018-09-18 22:42:38 +02:00
|
|
|
return context.WithTimeout(context.Background(), DefaultTimeout)
|
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
func parseAttrs(attrs []string) *values.Object {
|
2018-09-18 22:42:38 +02:00
|
|
|
var attr values.String
|
|
|
|
|
|
|
|
res := values.NewObject()
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
for _, el := range attrs {
|
2018-09-18 22:42:38 +02:00
|
|
|
str := values.NewString(el)
|
|
|
|
|
|
|
|
if common.IsAttribute(el) {
|
|
|
|
attr = str
|
|
|
|
res.Set(str, values.EmptyString)
|
|
|
|
} else {
|
|
|
|
current, ok := res.Get(attr)
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
res.Set(attr, current.(values.String).Concat(values.SpaceString).Concat(str))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
func loadNodes(client *cdp.Client, nodes []dom.NodeID) *values.Array {
|
|
|
|
arr := values.NewArray(len(nodes))
|
|
|
|
|
|
|
|
for _, id := range nodes {
|
|
|
|
child, err := LoadElement(client, id)
|
2018-09-18 22:42:38 +02:00
|
|
|
|
2018-09-25 17:43:58 +02:00
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
arr.Push(child)
|
|
|
|
}
|
|
|
|
|
|
|
|
return arr
|
2018-09-18 22:42:38 +02:00
|
|
|
}
|