Files
kyle 9444a35322 p1: /v2/scrape, /v1/fetch, auth, CLI, contract tests
Single Fetcher.Fetch on the Guard that holds DenyPrefixes. skipTlsVerification
is ignored. No 502. keys add writes the secret only to --out. Release binary
has no -test-allow-prefix.
2026-09-22 21:54:01 -07:00

623 lines
16 KiB
Go

package dom
import (
"bytes"
"regexp"
"strings"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
var (
rxPunctuation = regexp.MustCompile(`\s+([.?!,;])\s*(\S*)`)
rxTempNewline = regexp.MustCompile(`\s*\|\\/\|\s*`)
rxDisplayNone = regexp.MustCompile(`(?i)display:\s*none`)
rxVisibilityHidden = regexp.MustCompile(`(?i)visibility:\s*(:?hidden|collapse)`)
)
// QuerySelectorAll returns array of document's elements that match
// the specified group of selectors.
func QuerySelectorAll(doc *html.Node, selectors string) []*html.Node {
matcher, err := cascadia.ParseGroup(selectors)
if err != nil {
return nil
}
return cascadia.QueryAll(doc, matcher)
}
// QuerySelector returns the first document's element that match
// the specified group of selectors.
func QuerySelector(doc *html.Node, selectors string) *html.Node {
matcher, err := cascadia.ParseGroup(selectors)
if err != nil {
return nil
}
return cascadia.Query(doc, matcher)
}
// GetElementByID returns a Node object representing the element whose id
// property matches the specified string.
func GetElementByID(doc *html.Node, id string) *html.Node {
if id == "" {
return nil
}
var results []*html.Node
var finder func(*html.Node)
finder = func(node *html.Node) {
nodeID := GetAttribute(node, "id")
nodeID = strings.TrimSpace(nodeID)
if node.Type == html.ElementNode && nodeID == id {
results = append(results, node)
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
finder(child)
}
}
for child := doc.FirstChild; child != nil; child = child.NextSibling {
finder(child)
if len(results) > 0 {
return results[0]
}
}
return nil
}
// GetElementsByClassName returns an array of all child elements which
// have all of the given class name(s).
func GetElementsByClassName(doc *html.Node, classNames string) []*html.Node {
// Convert class name to map
classes := map[string]struct{}{}
for _, class := range strings.Fields(classNames) {
classes[class] = struct{}{}
}
nClasses := len(classes)
if nClasses == 0 {
return nil
}
// Create finder method
var results []*html.Node
var finder func(*html.Node)
allClassExist := func(node *html.Node) bool {
matchCount := 0
nodeClasses := GetAttribute(node, "class")
for _, nodeClass := range strings.Fields(nodeClasses) {
if _, exist := classes[nodeClass]; exist {
matchCount++
}
}
return matchCount == nClasses
}
finder = func(node *html.Node) {
if node.Type == html.ElementNode && allClassExist(node) {
results = append(results, node)
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
finder(child)
}
}
// Check all nodes
for child := doc.FirstChild; child != nil; child = child.NextSibling {
finder(child)
}
return results
}
// GetElementsByTagName returns a collection of all elements in the document with
// the specified tag name, as an array of Node object.
// The special tag "*" will represents all elements.
func GetElementsByTagName(doc *html.Node, tagName string) []*html.Node {
var results []*html.Node
var finder func(*html.Node)
finder = func(node *html.Node) {
if node.Type == html.ElementNode && (tagName == "*" || node.Data == tagName) {
results = append(results, node)
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
finder(child)
}
}
for child := doc.FirstChild; child != nil; child = child.NextSibling {
finder(child)
}
return results
}
// CreateElement creates a new ElementNode with specified tag.
func CreateElement(tagName string) *html.Node {
return &html.Node{
Type: html.ElementNode,
Data: tagName,
}
}
// CreateTextNode creates a new Text node.
func CreateTextNode(data string) *html.Node {
return &html.Node{
Type: html.TextNode,
Data: data,
}
}
// TagName returns the tag name of a Node.
// If it's not ElementNode, return empty string.
func TagName(node *html.Node) string {
if node == nil {
return ""
}
if node.Type != html.ElementNode {
return ""
}
return node.Data
}
// GetAttribute returns the value of a specified attribute on
// the element. If the given attribute does not exist, the value
// returned will be an empty string.
func GetAttribute(node *html.Node, attrName string) string {
for i := 0; i < len(node.Attr); i++ {
if node.Attr[i].Key == attrName {
return node.Attr[i].Val
}
}
return ""
}
// SetAttribute sets attribute for node. If attribute already exists,
// it will be replaced.
func SetAttribute(node *html.Node, attrName string, attrValue string) {
attrIdx := -1
for i := 0; i < len(node.Attr); i++ {
if node.Attr[i].Key == attrName {
attrIdx = i
break
}
}
if attrIdx >= 0 {
node.Attr[attrIdx].Val = attrValue
} else {
node.Attr = append(node.Attr, html.Attribute{
Key: attrName,
Val: attrValue,
})
}
}
// RemoveAttribute removes attribute with given name.
func RemoveAttribute(node *html.Node, attrName string) {
attrIdx := -1
for i := 0; i < len(node.Attr); i++ {
if node.Attr[i].Key == attrName {
attrIdx = i
break
}
}
if attrIdx >= 0 {
a := node.Attr
a = append(a[:attrIdx], a[attrIdx+1:]...)
node.Attr = a
}
}
// HasAttribute returns a Boolean value indicating whether the
// specified node has the specified attribute or not.
func HasAttribute(node *html.Node, attrName string) bool {
for i := 0; i < len(node.Attr); i++ {
if node.Attr[i].Key == attrName {
return true
}
}
return false
}
// TextContent returns the text content of the specified node,
// and all its descendants.
func TextContent(node *html.Node) string {
var buffer bytes.Buffer
var finder func(*html.Node)
finder = func(n *html.Node) {
if n.Type == html.TextNode {
buffer.WriteString(n.Data)
}
for child := n.FirstChild; child != nil; child = child.NextSibling {
finder(child)
}
}
finder(node)
return buffer.String()
}
// InnerText in JS used to capture text from an element while excluding text from hidden
// children. A child is hidden if it's computed width is 0, whether because its CSS (e.g
// `display: none`, `visibility: hidden`, etc), or if the child has `hidden` attribute.
// Since we can't compute stylesheet, we only look at `hidden` attribute and inline style.
//
// Besides excluding text from hidden children, difference between this function and
// `TextContent` is the latter will skip <br> tag while this function will preserve
// <br> as newline.
func InnerText(node *html.Node) string {
var buffer bytes.Buffer
var finder func(*html.Node)
finder = func(n *html.Node) {
switch n.Type {
case html.TextNode:
buffer.WriteString(" " + n.Data + " ")
case html.ElementNode:
if n.Data == "br" {
buffer.WriteString(`|\/|`)
return
}
if HasAttribute(n, "hidden") {
return
}
styleAttr := GetAttribute(n, "style")
if rxDisplayNone.MatchString(styleAttr) || rxVisibilityHidden.MatchString(styleAttr) {
return
}
}
for child := n.FirstChild; child != nil; child = child.NextSibling {
finder(child)
}
}
finder(node)
text := buffer.String()
text = strings.Join(strings.Fields(text), " ")
text = rxPunctuation.ReplaceAllString(text, "$1 $2")
text = rxTempNewline.ReplaceAllString(text, "\n")
return text
}
// OuterHTML returns an HTML serialization of the element and its descendants.
// The returned HTML value is escaped.
func OuterHTML(node *html.Node) string {
if node == nil {
return ""
}
var buffer bytes.Buffer
err := html.Render(&buffer, node)
if err != nil {
return ""
}
return buffer.String()
}
// InnerHTML returns the HTML content (inner HTML) of an element.
// The returned HTML value is escaped.
func InnerHTML(node *html.Node) string {
var err error
var buffer bytes.Buffer
if node == nil {
return ""
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
err = html.Render(&buffer, child)
if err != nil {
return ""
}
}
return strings.TrimSpace(buffer.String())
}
// DocumentElement returns the Element that is the root element
// of the document. Since we are working with HTML document,
// the root will be <html> element for HTML documents).
func DocumentElement(doc *html.Node) *html.Node {
if nodes := GetElementsByTagName(doc, "html"); len(nodes) > 0 {
return nodes[0]
}
return nil
}
// ID returns the value of the id attribute of the specified element.
func ID(node *html.Node) string {
id := GetAttribute(node, "id")
id = strings.TrimSpace(id)
return id
}
// ClassName returns the value of the class attribute of
// the specified element.
func ClassName(node *html.Node) string {
className := GetAttribute(node, "class")
className = strings.TrimSpace(className)
className = strings.Join(strings.Fields(className), " ")
return className
}
// Children returns an HTMLCollection of the direct child elements of Node.
func Children(node *html.Node) []*html.Node {
var children []*html.Node
if node == nil {
return nil
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
children = append(children, child)
}
}
return children
}
// ChildNodes returns list of a node's direct children.
func ChildNodes(node *html.Node) []*html.Node {
var childNodes []*html.Node
for child := node.FirstChild; child != nil; child = child.NextSibling {
childNodes = append(childNodes, child)
}
return childNodes
}
// FirstElementChild returns the object's first child Element,
// or nil if there are no child elements.
func FirstElementChild(node *html.Node) *html.Node {
for child := node.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
return child
}
}
return nil
}
// PreviousElementSibling returns the the Element immediately prior
// to the specified one in its parent's children list, or null if
// the specified element is the first one in the list.
func PreviousElementSibling(node *html.Node) *html.Node {
for sibling := node.PrevSibling; sibling != nil; sibling = sibling.PrevSibling {
if sibling.Type == html.ElementNode {
return sibling
}
}
return nil
}
// NextElementSibling returns the Element immediately following
// the specified one in its parent's children list, or nil if the
// specified Element is the last one in the list.
func NextElementSibling(node *html.Node) *html.Node {
for sibling := node.NextSibling; sibling != nil; sibling = sibling.NextSibling {
if sibling.Type == html.ElementNode {
return sibling
}
}
return nil
}
// AppendChild adds a node to the end of the list of children of a
// specified parent node. If the given child is a reference to an
// existing node in the document, AppendChild() moves it from its
// current position to the new position.
func AppendChild(node *html.Node, child *html.Node) {
// Make sure node is not void
if !IsVoidElement(node) {
DetachChild(child)
node.AppendChild(child)
}
}
// PrependChild works like AppendChild() except it adds a node to the
// beginning of the list of children of a specified parent node.
func PrependChild(node *html.Node, child *html.Node) {
// Make sure node is not void
if !IsVoidElement(node) {
DetachChild(child)
if node.FirstChild != nil {
node.InsertBefore(child, node.FirstChild)
} else {
node.AppendChild(child)
}
}
}
// ReplaceChild replaces a child node within the given (parent) node.
// If the new child is already exist in document, ReplaceChild() will move it
// from its current position to replace old child. Returns both the new and old child.
//
// TODO: I'm note sure but I *think* there are some issues here. Check later I guess.
func ReplaceChild(parent *html.Node, newChild *html.Node, oldChild *html.Node) (*html.Node, *html.Node) {
// Make sure parent is specified and not void
if parent == nil && !IsVoidElement(parent) {
return newChild, oldChild
}
// Make sure the specified parent IS the parent of the old child
if oldChild.Parent != parent {
return newChild, oldChild
}
// Detach the new child
DetachChild(newChild)
parent.InsertBefore(newChild, oldChild)
parent.RemoveChild(oldChild)
return newChild, oldChild
}
// IncludeNode determines if node is included inside nodeList.
func IncludeNode(nodeList []*html.Node, node *html.Node) bool {
for i := 0; i < len(nodeList); i++ {
if nodeList[i] == node {
return true
}
}
return false
}
// Clone returns a clone of the node and (if specified) its children.
// However, it will be detached from the original's parents and siblings.
func Clone(src *html.Node, deep bool) *html.Node {
clone := &html.Node{
Type: src.Type,
DataAtom: src.DataAtom,
Data: src.Data,
Attr: append([]html.Attribute{}, src.Attr...),
}
if deep {
for child := src.FirstChild; child != nil; child = child.NextSibling {
clone.AppendChild(Clone(child, deep))
}
}
return clone
}
// GetAllNodesWithTag is wrapper for GetElementsByTagName()
// which allow to get several tags at once.
func GetAllNodesWithTag(node *html.Node, tagNames ...string) []*html.Node {
var result []*html.Node
for i := 0; i < len(tagNames); i++ {
result = append(result, GetElementsByTagName(node, tagNames[i])...)
}
return result
}
// ForEachNode iterates over a NodeList and runs fn on each node.
func ForEachNode(nodeList []*html.Node, fn func(*html.Node, int)) {
for i := 0; i < len(nodeList); i++ {
fn(nodeList[i], i)
}
}
// RemoveNodes iterates over a NodeList, calls `filterFn` for each node
// and removes node if function returned `true`. If function is not
// passed, removes all the nodes in node list.
func RemoveNodes(nodeList []*html.Node, filterFn func(*html.Node) bool) {
for i := len(nodeList) - 1; i >= 0; i-- {
node := nodeList[i]
parentNode := node.Parent
if parentNode != nil && (filterFn == nil || filterFn(node)) {
parentNode.RemoveChild(node)
}
}
}
// SetTextContent sets the text content of the specified node.
func SetTextContent(node *html.Node, text string) {
if IsVoidElement(node) {
return
}
child := node.FirstChild
for child != nil {
nextSibling := child.NextSibling
node.RemoveChild(child)
child = nextSibling
}
node.AppendChild(&html.Node{
Type: html.TextNode,
Data: text,
})
}
// SetInnerHTML sets inner HTML of the specified node.
func SetInnerHTML(node *html.Node, rawHTML string) {
// Parse raw HTML
parsedHTML, err := html.Parse(strings.NewReader(rawHTML))
if err != nil || parsedHTML == nil {
return
}
// Remove node's current children
child := node.FirstChild
for child != nil {
nextSibling := child.NextSibling
node.RemoveChild(child)
child = nextSibling
}
// Put content of parsed HTML to the node
if body := QuerySelector(parsedHTML, "body"); body != nil {
bodyChild := body.FirstChild
for bodyChild != nil {
nextSibling := bodyChild.NextSibling
AppendChild(node, bodyChild)
bodyChild = nextSibling
}
}
}
// IsVoidElement check whether a node can have any contents or not.
// Return true if element is void (can't have any children).
func IsVoidElement(n *html.Node) bool {
// If it's not element, it's void
if n.Type != html.ElementNode {
return true
}
// Check tag name
switch n.Data {
case "area", "base", "br", "col", "embed", "hr",
"img", "input", "keygen", "link", "meta",
"param", "source", "track", "wbr":
return true
default:
return false
}
}
func DetachChild(child *html.Node) {
if child.Parent != nil || child.PrevSibling != nil || child.NextSibling != nil {
if child.Parent != nil {
if child.Parent.FirstChild == child {
child.Parent.FirstChild = child.NextSibling
}
if child.Parent.LastChild == child {
child.Parent.LastChild = child.PrevSibling
}
}
if child.PrevSibling != nil {
child.PrevSibling.NextSibling = child.NextSibling
}
if child.NextSibling != nil {
child.NextSibling.PrevSibling = child.PrevSibling
}
child.Parent = nil
child.PrevSibling = nil
child.NextSibling = nil
}
}