diff --git a/.travis.yml b/.travis.yml index cb8f4d6..41b1a36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,7 @@ go: - 1.1 - 1.2 - 1.3 + - 1.4 + - 1.5 + - 1.6 - tip diff --git a/LICENSE b/LICENSE index 6455cec..f743d37 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2014, Martin Angers & Contributors +Copyright (c) 2012-2016, Martin Angers & Contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index df5ade6..b53a353 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,4 @@ -# goquery - a little like that j-thing, only in Go - -[![build status](https://secure.travis-ci.org/PuerkitoBio/goquery.png)](http://travis-ci.org/PuerkitoBio/goquery) - -[![GoDoc](https://godoc.org/github.com/PuerkitoBio/goquery?status.png)](http://godoc.org/github.com/PuerkitoBio/goquery) +# goquery - a little like that j-thing, only in Go [![build status](https://secure.travis-ci.org/PuerkitoBio/goquery.png)](http://travis-ci.org/PuerkitoBio/goquery) [![GoDoc](https://godoc.org/github.com/PuerkitoBio/goquery?status.png)](http://godoc.org/github.com/PuerkitoBio/goquery) goquery brings a syntax and a set of features similar to [jQuery][] to the [Go language][go]. It is based on Go's [net/html package][html] and the CSS Selector library [cascadia][]. Since the net/html parser returns nodes, and not a full-featured DOM tree, jQuery's stateful manipulation functions (like height(), css(), detach()) have been left off. @@ -30,6 +26,7 @@ Please note that because of the net/html dependency, goquery requires Go1.1+. **Note that goquery's API is now stable, and will not break.** +* **2016-06-15** : Invalid selector strings internally compile to a `Matcher` implementation that never matches any node (instead of a panic). So for example, `doc.Find("~")` returns an empty `*Selection` object. * **2016-02-02** : Add `NodeName` utility function similar to the DOM's `nodeName` property. It returns the tag name of the first element in a selection, and other relevant values of non-element nodes (see godoc for details). Add `OuterHtml` utility function similar to the DOM's `outerHTML` property (named `OuterHtml` in small caps for consistency with the existing `Html` method on the `Selection`). * **2015-04-20** : Add `AttrOr` helper method to return the attribute's value or a default value if absent. Thanks to [piotrkowalczuk][piotr]. * **2015-02-04** : Add more manipulation functions - Prepend* - thanks again to [Andrew Stone][thatguystone]. @@ -61,7 +58,12 @@ Utility functions that are not in jQuery but are useful in Go are implemented as The complete [godoc reference documentation can be found here][doc]. -Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details. +Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details. Invalid selector strings compile to a `Matcher` that fails to match any node. Behaviour of the various functions that take a selector string as argument follows from that fact, e.g. (where `~` is an invalid selector string): + +* `Find("~")` returns an empty selection because the selector string doesn't match anything. +* `Add("~")` returns a new selection that holds the same nodes as the original selection, because it didn't add any node (selector string didn't match anything). +* `ParentsFiltered("~")` returns an empty selection because the selector string doesn't match anything. +* `ParentsUntil("~")` returns all parents of the selection because the selector string didn't match any element to stop before the top element. ## Examples diff --git a/array_test.go b/array_test.go index 1dda525..8e50f75 100644 --- a/array_test.go +++ b/array_test.go @@ -14,6 +14,11 @@ func TestFirstEmpty(t *testing.T) { assertLength(t, sel.Nodes, 0) } +func TestFirstInvalid(t *testing.T) { + sel := Doc().Find("").First() + assertLength(t, sel.Nodes, 0) +} + func TestFirstRollback(t *testing.T) { sel := Doc().Find(".pvk-content") sel2 := sel.First().End() @@ -36,6 +41,11 @@ func TestLastEmpty(t *testing.T) { assertLength(t, sel.Nodes, 0) } +func TestLastInvalid(t *testing.T) { + sel := Doc().Find("").Last() + assertLength(t, sel.Nodes, 0) +} + func TestLastRollback(t *testing.T) { sel := Doc().Find(".pvk-content") sel2 := sel.Last().End() @@ -63,6 +73,11 @@ func TestEqEmpty(t *testing.T) { assertLength(t, sel.Nodes, 0) } +func TestEqInvalid(t *testing.T) { + sel := Doc().Find("").Eq(0) + assertLength(t, sel.Nodes, 0) +} + func TestEqInvalidPositive(t *testing.T) { sel := Doc().Find(".pvk-content").Eq(3) assertLength(t, sel.Nodes, 0) @@ -85,6 +100,16 @@ func TestSlice(t *testing.T) { assertLength(t, sel.Nodes, 2) } +func TestSliceEmpty(t *testing.T) { + defer assertPanic(t) + Doc().Find("x").Slice(0, 2) +} + +func TestSliceInvalid(t *testing.T) { + defer assertPanic(t) + Doc().Find("").Slice(0, 2) +} + func TestSliceOutOfBounds(t *testing.T) { defer assertPanic(t) Doc().Find(".pvk-content").Slice(2, 12) @@ -157,6 +182,13 @@ func TestIndexSelector(t *testing.T) { } } +func TestIndexSelectorInvalid(t *testing.T) { + sel := Doc().Find(".hero-unit") + if i := sel.IndexSelector(""); i != -1 { + t.Errorf("Expected index of -1, got %v.", i) + } +} + func TestIndexOfNode(t *testing.T) { sel := Doc().Find("div.pvk-gutter") if i := sel.IndexOfNode(sel.Nodes[1]); i != 1 { diff --git a/doc.go b/doc.go index 62f7ee9..71146a7 100644 --- a/doc.go +++ b/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2014, Martin Angers & Contributors +// Copyright (c) 2012-2016, Martin Angers & Contributors // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, diff --git a/expand.go b/expand.go index c3cadff..0f82a99 100644 --- a/expand.go +++ b/expand.go @@ -1,16 +1,13 @@ package goquery -import ( - "github.com/andybalholm/cascadia" - "golang.org/x/net/html" -) +import "golang.org/x/net/html" // Add adds the selector string's matching nodes to those in the current // selection and returns a new Selection object. // The selector string is run in the context of the document of the current // Selection object. func (s *Selection) Add(selector string) *Selection { - return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, cascadia.MustCompile(selector))...) + return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, compileMatcher(selector))...) } // AddMatcher adds the matcher's matching nodes to those in the current diff --git a/expand_test.go b/expand_test.go index cff6996..e53b83f 100644 --- a/expand_test.go +++ b/expand_test.go @@ -9,6 +9,16 @@ func TestAdd(t *testing.T) { assertLength(t, sel.Nodes, 19) } +func TestAddInvalid(t *testing.T) { + sel1 := Doc().Find("div.row-fluid") + sel2 := sel1.Add("") + assertLength(t, sel1.Nodes, 9) + assertLength(t, sel2.Nodes, 9) + if sel1 == sel2 { + t.Errorf("selections should not be the same") + } +} + func TestAddRollback(t *testing.T) { sel := Doc().Find(".pvk-content") sel2 := sel.Add("a").End() diff --git a/filter.go b/filter.go index f7e0704..3c8cc42 100644 --- a/filter.go +++ b/filter.go @@ -1,19 +1,16 @@ package goquery -import ( - "github.com/andybalholm/cascadia" - "golang.org/x/net/html" -) +import "golang.org/x/net/html" // Filter reduces the set of matched elements to those that match the selector string. // It returns a new Selection object for this subset of matching elements. func (s *Selection) Filter(selector string) *Selection { - return s.FilterMatcher(cascadia.MustCompile(selector)) + return s.FilterMatcher(compileMatcher(selector)) } // FilterMatcher reduces the set of matched elements to those that match -// the given matcher. -// It returns a new Selection object for this subset of matching elements. +// the given matcher. It returns a new Selection object for this subset +// of matching elements. func (s *Selection) FilterMatcher(m Matcher) *Selection { return pushStack(s, winnow(s, m, true)) } @@ -21,7 +18,7 @@ func (s *Selection) FilterMatcher(m Matcher) *Selection { // Not removes elements from the Selection that match the selector string. // It returns a new Selection object with the matching elements removed. func (s *Selection) Not(selector string) *Selection { - return s.NotMatcher(cascadia.MustCompile(selector)) + return s.NotMatcher(compileMatcher(selector)) } // NotMatcher removes elements from the Selection that match the given matcher. diff --git a/filter_test.go b/filter_test.go index d5978b5..f663c08 100644 --- a/filter_test.go +++ b/filter_test.go @@ -14,6 +14,11 @@ func TestFilterNone(t *testing.T) { assertLength(t, sel.Nodes, 0) } +func TestFilterInvalid(t *testing.T) { + sel := Doc().Find(".span12").Filter("") + assertLength(t, sel.Nodes, 0) +} + func TestFilterRollback(t *testing.T) { sel := Doc().Find(".pvk-content") sel2 := sel.Filter(".alert").End() @@ -74,6 +79,11 @@ func TestNot(t *testing.T) { assertLength(t, sel.Nodes, 1) } +func TestNotInvalid(t *testing.T) { + sel := Doc().Find(".span12").Not("") + assertLength(t, sel.Nodes, 2) +} + func TestNotRollback(t *testing.T) { sel := Doc().Find(".span12") sel2 := sel.Not(".alert").End() @@ -145,6 +155,11 @@ func TestHas(t *testing.T) { // Has() returns the high-level .container-fluid div, and the one that is the immediate parent of center-content } +func TestHasInvalid(t *testing.T) { + sel := Doc().Find(".container-fluid").Has("") + assertLength(t, sel.Nodes, 0) +} + func TestHasRollback(t *testing.T) { sel := Doc().Find(".container-fluid") sel2 := sel.Has(".center-content").End() diff --git a/manipulation.go b/manipulation.go index 9f150f6..3602a5a 100644 --- a/manipulation.go +++ b/manipulation.go @@ -3,7 +3,6 @@ package goquery import ( "strings" - "github.com/andybalholm/cascadia" "golang.org/x/net/html" ) @@ -15,7 +14,7 @@ import ( // // This follows the same rules as Selection.Append. func (s *Selection) After(selector string) *Selection { - return s.AfterMatcher(cascadia.MustCompile(selector)) + return s.AfterMatcher(compileMatcher(selector)) } // AfterMatcher applies the matcher from the root document and inserts the matched elements @@ -66,7 +65,7 @@ func (s *Selection) AfterNodes(ns ...*html.Node) *Selection { // appended to all target locations except the last one, which will be moved // as noted in (2). func (s *Selection) Append(selector string) *Selection { - return s.AppendMatcher(cascadia.MustCompile(selector)) + return s.AppendMatcher(compileMatcher(selector)) } // AppendMatcher appends the elements specified by the matcher to the end of each element @@ -103,7 +102,7 @@ func (s *Selection) AppendNodes(ns ...*html.Node) *Selection { // // This follows the same rules as Selection.Append. func (s *Selection) Before(selector string) *Selection { - return s.BeforeMatcher(cascadia.MustCompile(selector)) + return s.BeforeMatcher(compileMatcher(selector)) } // BeforeMatcher inserts the matched elements before each element in the set of matched elements. @@ -165,7 +164,7 @@ func (s *Selection) Empty() *Selection { // Prepend prepends the elements specified by the selector to each element in // the set of matched elements, following the same rules as Append. func (s *Selection) Prepend(selector string) *Selection { - return s.PrependMatcher(cascadia.MustCompile(selector)) + return s.PrependMatcher(compileMatcher(selector)) } // PrependMatcher prepends the elements specified by the matcher to each @@ -216,7 +215,7 @@ func (s *Selection) Remove() *Selection { // RemoveFiltered removes the set of matched elements by selector. // It returns the Selection of removed nodes. func (s *Selection) RemoveFiltered(selector string) *Selection { - return s.RemoveMatcher(cascadia.MustCompile(selector)) + return s.RemoveMatcher(compileMatcher(selector)) } // RemoveMatcher removes the set of matched elements. @@ -231,7 +230,7 @@ func (s *Selection) RemoveMatcher(m Matcher) *Selection { // // This follows the same rules as Selection.Append. func (s *Selection) ReplaceWith(selector string) *Selection { - return s.ReplaceWithMatcher(cascadia.MustCompile(selector)) + return s.ReplaceWithMatcher(compileMatcher(selector)) } // ReplaceWithMatcher replaces each element in the set of matched elements with @@ -293,7 +292,7 @@ func (s *Selection) Unwrap() *Selection { // // It returns the original set of elements. func (s *Selection) Wrap(selector string) *Selection { - return s.WrapMatcher(cascadia.MustCompile(selector)) + return s.WrapMatcher(compileMatcher(selector)) } // WrapMatcher wraps each element in the set of matched elements inside the @@ -345,7 +344,7 @@ func (s *Selection) wrapNodes(ns ...*html.Node) *Selection { // // It returns the original set of elements. func (s *Selection) WrapAll(selector string) *Selection { - return s.WrapAllMatcher(cascadia.MustCompile(selector)) + return s.WrapAllMatcher(compileMatcher(selector)) } // WrapAllMatcher wraps a single HTML structure, matched by the given Matcher, @@ -415,7 +414,7 @@ func (s *Selection) WrapAllNode(n *html.Node) *Selection { // // It returns the original set of elements. func (s *Selection) WrapInner(selector string) *Selection { - return s.WrapInnerMatcher(cascadia.MustCompile(selector)) + return s.WrapInnerMatcher(compileMatcher(selector)) } // WrapInnerMatcher wraps an HTML structure, matched by the given selector, diff --git a/query.go b/query.go index 8a0ffcf..1a7f873 100644 --- a/query.go +++ b/query.go @@ -1,15 +1,12 @@ package goquery -import ( - "github.com/andybalholm/cascadia" - "golang.org/x/net/html" -) +import "golang.org/x/net/html" // Is checks the current matched set of elements against a selector and // returns true if at least one of these elements matches. func (s *Selection) Is(selector string) bool { if len(s.Nodes) > 0 { - return s.IsMatcher(cascadia.MustCompile(selector)) + return s.IsMatcher(compileMatcher(selector)) } return false diff --git a/query_test.go b/query_test.go index 2f40f42..54b2a2e 100644 --- a/query_test.go +++ b/query_test.go @@ -11,6 +11,13 @@ func TestIs(t *testing.T) { } } +func TestIsInvalid(t *testing.T) { + sel := Doc().Find(".footer p:nth-child(1)") + if sel.Is("") { + t.Error("Is should not succeed with invalid selector string") + } +} + func TestIsPositional(t *testing.T) { sel := Doc().Find(".footer p:nth-child(2)") if !sel.Is("p:nth-child(2)") { diff --git a/traversal.go b/traversal.go index 4f3d192..5ba8782 100644 --- a/traversal.go +++ b/traversal.go @@ -1,9 +1,6 @@ package goquery -import ( - "github.com/andybalholm/cascadia" - "golang.org/x/net/html" -) +import "golang.org/x/net/html" type siblingType int @@ -24,7 +21,7 @@ const ( // elements, filtered by a selector. It returns a new Selection object // containing these matched elements. func (s *Selection) Find(selector string) *Selection { - return pushStack(s, findWithMatcher(s.Nodes, cascadia.MustCompile(selector))) + return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector))) } // FindMatcher gets the descendants of each element in the current set of matched @@ -93,7 +90,7 @@ func (s *Selection) Children() *Selection { // filtered by the specified selector. It returns a new // Selection object containing these elements. func (s *Selection) ChildrenFiltered(selector string) *Selection { - return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), cascadia.MustCompile(selector)) + return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), compileMatcher(selector)) } // ChildrenMatcher gets the child elements of each element in the Selection, @@ -112,7 +109,7 @@ func (s *Selection) Parent() *Selection { // ParentFiltered gets the parent of each element in the Selection filtered by a // selector. It returns a new Selection object containing the matched elements. func (s *Selection) ParentFiltered(selector string) *Selection { - return filterAndPush(s, getParentNodes(s.Nodes), cascadia.MustCompile(selector)) + return filterAndPush(s, getParentNodes(s.Nodes), compileMatcher(selector)) } // ParentMatcher gets the parent of each element in the Selection filtered by a @@ -124,7 +121,7 @@ func (s *Selection) ParentMatcher(m Matcher) *Selection { // Closest gets the first element that matches the selector by testing the // element itself and traversing up through its ancestors in the DOM tree. func (s *Selection) Closest(selector string) *Selection { - cs := cascadia.MustCompile(selector) + cs := compileMatcher(selector) return s.ClosestMatcher(cs) } @@ -177,7 +174,7 @@ func (s *Selection) Parents() *Selection { // ParentsFiltered gets the ancestors of each element in the current // Selection. It returns a new Selection object with the matched elements. func (s *Selection) ParentsFiltered(selector string) *Selection { - return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), cascadia.MustCompile(selector)) + return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), compileMatcher(selector)) } // ParentsMatcher gets the ancestors of each element in the current @@ -190,7 +187,7 @@ func (s *Selection) ParentsMatcher(m Matcher) *Selection { // not including the element matched by the selector. It returns a new Selection // object containing the matched elements. func (s *Selection) ParentsUntil(selector string) *Selection { - return pushStack(s, getParentsNodes(s.Nodes, cascadia.MustCompile(selector), nil)) + return pushStack(s, getParentsNodes(s.Nodes, compileMatcher(selector), nil)) } // ParentsUntilMatcher gets the ancestors of each element in the Selection, up to but @@ -221,7 +218,7 @@ func (s *Selection) ParentsUntilNodes(nodes ...*html.Node) *Selection { // results based on a selector string. It returns a new Selection // object containing the matched elements. func (s *Selection) ParentsFilteredUntil(filterSelector, untilSelector string) *Selection { - return filterAndPush(s, getParentsNodes(s.Nodes, cascadia.MustCompile(untilSelector), nil), cascadia.MustCompile(filterSelector)) + return filterAndPush(s, getParentsNodes(s.Nodes, compileMatcher(untilSelector), nil), compileMatcher(filterSelector)) } // ParentsFilteredUntilMatcher is like ParentsUntilMatcher, with the option to filter the @@ -234,7 +231,7 @@ func (s *Selection) ParentsFilteredUntilMatcher(filter, until Matcher) *Selectio // option to filter the results based on a selector string. It returns a new // Selection object containing the matched elements. func (s *Selection) ParentsFilteredUntilSelection(filterSelector string, sel *Selection) *Selection { - return s.ParentsMatcherUntilSelection(cascadia.MustCompile(filterSelector), sel) + return s.ParentsMatcherUntilSelection(compileMatcher(filterSelector), sel) } // ParentsMatcherUntilSelection is like ParentsUntilSelection, with the @@ -251,7 +248,7 @@ func (s *Selection) ParentsMatcherUntilSelection(filter Matcher, sel *Selection) // option to filter the results based on a selector string. It returns a new // Selection object containing the matched elements. func (s *Selection) ParentsFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection { - return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), cascadia.MustCompile(filterSelector)) + return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), compileMatcher(filterSelector)) } // ParentsMatcherUntilNodes is like ParentsUntilNodes, with the @@ -271,7 +268,7 @@ func (s *Selection) Siblings() *Selection { // filtered by a selector. It returns a new Selection object containing the // matched elements. func (s *Selection) SiblingsFiltered(selector string) *Selection { - return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), cascadia.MustCompile(selector)) + return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), compileMatcher(selector)) } // SiblingsMatcher gets the siblings of each element in the Selection @@ -291,7 +288,7 @@ func (s *Selection) Next() *Selection { // Selection filtered by a selector. It returns a new Selection object // containing the matched elements. func (s *Selection) NextFiltered(selector string) *Selection { - return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), cascadia.MustCompile(selector)) + return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), compileMatcher(selector)) } // NextMatcher gets the immediately following sibling of each element in the @@ -311,7 +308,7 @@ func (s *Selection) NextAll() *Selection { // Selection filtered by a selector. It returns a new Selection object // containing the matched elements. func (s *Selection) NextAllFiltered(selector string) *Selection { - return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), cascadia.MustCompile(selector)) + return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), compileMatcher(selector)) } // NextAllMatcher gets all the following siblings of each element in the @@ -331,7 +328,7 @@ func (s *Selection) Prev() *Selection { // Selection filtered by a selector. It returns a new Selection object // containing the matched elements. func (s *Selection) PrevFiltered(selector string) *Selection { - return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), cascadia.MustCompile(selector)) + return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), compileMatcher(selector)) } // PrevMatcher gets the immediately preceding sibling of each element in the @@ -351,7 +348,7 @@ func (s *Selection) PrevAll() *Selection { // Selection filtered by a selector. It returns a new Selection object // containing the matched elements. func (s *Selection) PrevAllFiltered(selector string) *Selection { - return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), cascadia.MustCompile(selector)) + return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), compileMatcher(selector)) } // PrevAllMatcher gets all the preceding siblings of each element in the @@ -366,7 +363,7 @@ func (s *Selection) PrevAllMatcher(m Matcher) *Selection { // object containing the matched elements. func (s *Selection) NextUntil(selector string) *Selection { return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil, - cascadia.MustCompile(selector), nil)) + compileMatcher(selector), nil)) } // NextUntilMatcher gets all following siblings of each element up to but not @@ -400,7 +397,7 @@ func (s *Selection) NextUntilNodes(nodes ...*html.Node) *Selection { // object containing the matched elements. func (s *Selection) PrevUntil(selector string) *Selection { return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil, - cascadia.MustCompile(selector), nil)) + compileMatcher(selector), nil)) } // PrevUntilMatcher gets all preceding siblings of each element up to but not @@ -434,7 +431,7 @@ func (s *Selection) PrevUntilNodes(nodes ...*html.Node) *Selection { // It returns a new Selection object containing the matched elements. func (s *Selection) NextFilteredUntil(filterSelector, untilSelector string) *Selection { return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil, - cascadia.MustCompile(untilSelector), nil), cascadia.MustCompile(filterSelector)) + compileMatcher(untilSelector), nil), compileMatcher(filterSelector)) } // NextFilteredUntilMatcher is like NextUntilMatcher, with the option to filter @@ -449,7 +446,7 @@ func (s *Selection) NextFilteredUntilMatcher(filter, until Matcher) *Selection { // option to filter the results based on a selector string. It returns a new // Selection object containing the matched elements. func (s *Selection) NextFilteredUntilSelection(filterSelector string, sel *Selection) *Selection { - return s.NextMatcherUntilSelection(cascadia.MustCompile(filterSelector), sel) + return s.NextMatcherUntilSelection(compileMatcher(filterSelector), sel) } // NextMatcherUntilSelection is like NextUntilSelection, with the @@ -467,7 +464,7 @@ func (s *Selection) NextMatcherUntilSelection(filter Matcher, sel *Selection) *S // Selection object containing the matched elements. func (s *Selection) NextFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection { return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil, - nil, nodes), cascadia.MustCompile(filterSelector)) + nil, nodes), compileMatcher(filterSelector)) } // NextMatcherUntilNodes is like NextUntilNodes, with the @@ -483,7 +480,7 @@ func (s *Selection) NextMatcherUntilNodes(filter Matcher, nodes ...*html.Node) * // It returns a new Selection object containing the matched elements. func (s *Selection) PrevFilteredUntil(filterSelector, untilSelector string) *Selection { return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil, - cascadia.MustCompile(untilSelector), nil), cascadia.MustCompile(filterSelector)) + compileMatcher(untilSelector), nil), compileMatcher(filterSelector)) } // PrevFilteredUntilMatcher is like PrevUntilMatcher, with the option to filter @@ -498,7 +495,7 @@ func (s *Selection) PrevFilteredUntilMatcher(filter, until Matcher) *Selection { // option to filter the results based on a selector string. It returns a new // Selection object containing the matched elements. func (s *Selection) PrevFilteredUntilSelection(filterSelector string, sel *Selection) *Selection { - return s.PrevMatcherUntilSelection(cascadia.MustCompile(filterSelector), sel) + return s.PrevMatcherUntilSelection(compileMatcher(filterSelector), sel) } // PrevMatcherUntilSelection is like PrevUntilSelection, with the @@ -516,7 +513,7 @@ func (s *Selection) PrevMatcherUntilSelection(filter Matcher, sel *Selection) *S // Selection object containing the matched elements. func (s *Selection) PrevFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection { return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil, - nil, nodes), cascadia.MustCompile(filterSelector)) + nil, nodes), compileMatcher(filterSelector)) } // PrevMatcherUntilNodes is like PrevUntilNodes, with the diff --git a/traversal_test.go b/traversal_test.go index 00fb5cd..856e71d 100644 --- a/traversal_test.go +++ b/traversal_test.go @@ -21,9 +21,9 @@ func TestFindNotSelf(t *testing.T) { assertLength(t, sel.Nodes, 0) } -func TestFindInvalidSelector(t *testing.T) { - defer assertPanic(t) - Doc().Find(":+ ^") +func TestFindInvalid(t *testing.T) { + sel := Doc().Find(":+ ^") + assertLength(t, sel.Nodes, 0) } func TestChainedFind(t *testing.T) { @@ -31,6 +31,11 @@ func TestChainedFind(t *testing.T) { assertLength(t, sel.Nodes, 4) } +func TestChainedFindInvalid(t *testing.T) { + sel := Doc().Find("div.hero-unit").Find("") + assertLength(t, sel.Nodes, 0) +} + func TestChildren(t *testing.T) { sel := Doc().Find(".pvk-content").Children() assertLength(t, sel.Nodes, 5) @@ -58,6 +63,11 @@ func TestChildrenFiltered(t *testing.T) { assertLength(t, sel.Nodes, 1) } +func TestChildrenFilteredInvalid(t *testing.T) { + sel := Doc().Find(".pvk-content").ChildrenFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestChildrenFilteredRollback(t *testing.T) { sel := Doc().Find(".pvk-content") sel2 := sel.ChildrenFiltered(".hero-unit").End() @@ -69,6 +79,11 @@ func TestContentsFiltered(t *testing.T) { assertLength(t, sel.Nodes, 1) } +func TestContentsFilteredInvalid(t *testing.T) { + sel := Doc().Find(".pvk-content").ContentsFiltered("~") + assertLength(t, sel.Nodes, 0) +} + func TestContentsFilteredRollback(t *testing.T) { sel := Doc().Find(".pvk-content") sel2 := sel.ContentsFiltered(".hero-unit").End() @@ -102,6 +117,11 @@ func TestParentFiltered(t *testing.T) { assertClass(t, sel, "hero-unit") } +func TestParentFilteredInvalid(t *testing.T) { + sel := Doc().Find(".container-fluid").ParentFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestParentFilteredRollback(t *testing.T) { sel := Doc().Find(".container-fluid") sel2 := sel.ParentFiltered(".hero-unit").End() @@ -130,6 +150,11 @@ func TestParentsFiltered(t *testing.T) { assertLength(t, sel.Nodes, 1) } +func TestParentsFilteredInvalid(t *testing.T) { + sel := Doc().Find(".container-fluid").ParentsFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestParentsFilteredRollback(t *testing.T) { sel := Doc().Find(".container-fluid") sel2 := sel.ParentsFiltered("body").End() @@ -141,6 +166,11 @@ func TestParentsUntil(t *testing.T) { assertLength(t, sel.Nodes, 6) } +func TestParentsUntilInvalid(t *testing.T) { + sel := Doc().Find(".container-fluid").ParentsUntil("") + assertLength(t, sel.Nodes, 8) +} + func TestParentsUntilRollback(t *testing.T) { sel := Doc().Find(".container-fluid") sel2 := sel.ParentsUntil("body").End() @@ -180,6 +210,11 @@ func TestParentsFilteredUntil(t *testing.T) { assertLength(t, sel.Nodes, 2) } +func TestParentsFilteredUntilInvalid(t *testing.T) { + sel := Doc().Find(".container-fluid").ParentsFilteredUntil("", "") + assertLength(t, sel.Nodes, 0) +} + func TestParentsFilteredUntilRollback(t *testing.T) { sel := Doc().Find(".container-fluid") sel2 := sel.ParentsFilteredUntil(".pvk-content", "body").End() @@ -240,6 +275,11 @@ func TestSiblingsFiltered(t *testing.T) { assertLength(t, sel.Nodes, 3) } +func TestSiblingsFilteredInvalid(t *testing.T) { + sel := Doc().Find(".pvk-gutter").SiblingsFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestSiblingsFilteredRollback(t *testing.T) { sel := Doc().Find(".pvk-gutter") sel2 := sel.SiblingsFiltered(".pvk-content").End() @@ -272,6 +312,11 @@ func TestNextFiltered(t *testing.T) { assertLength(t, sel.Nodes, 2) } +func TestNextFilteredInvalid(t *testing.T) { + sel := Doc().Find(".container-fluid").NextFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestNextFilteredRollback(t *testing.T) { sel := Doc().Find(".container-fluid") sel2 := sel.NextFiltered("div").End() @@ -310,6 +355,11 @@ func TestPrevFiltered(t *testing.T) { assertLength(t, sel.Nodes, 5) } +func TestPrevFilteredInvalid(t *testing.T) { + sel := Doc().Find(".row-fluid").PrevFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestPrevFilteredRollback(t *testing.T) { sel := Doc().Find(".row-fluid") sel2 := sel.PrevFiltered(".row-fluid").End() @@ -342,6 +392,11 @@ func TestNextAllFiltered(t *testing.T) { assertLength(t, sel.Nodes, 2) } +func TestNextAllFilteredInvalid(t *testing.T) { + sel := Doc().Find("#cf2 .row-fluid").NextAllFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestNextAllFilteredRollback(t *testing.T) { sel := Doc().Find("#cf2 .row-fluid") sel2 := sel.NextAllFiltered("[ng-cloak]").End() @@ -380,6 +435,11 @@ func TestPrevAllFiltered(t *testing.T) { assertLength(t, sel.Nodes, 3) } +func TestPrevAllFilteredInvalid(t *testing.T) { + sel := Doc().Find(".pvk-gutter").PrevAllFiltered("") + assertLength(t, sel.Nodes, 0) +} + func TestPrevAllFilteredRollback(t *testing.T) { sel := Doc().Find(".pvk-gutter") sel2 := sel.PrevAllFiltered(".pvk-content").End() @@ -392,6 +452,11 @@ func TestNextUntil(t *testing.T) { assertSelectionIs(t, sel, "h4") } +func TestNextUntilInvalid(t *testing.T) { + sel := Doc().Find(".alert a").NextUntil("") + assertLength(t, sel.Nodes, 2) +} + func TestNextUntil2(t *testing.T) { sel := Doc().Find("#cf2-1").NextUntil("[ng-cloak]") assertLength(t, sel.Nodes, 1) @@ -446,6 +511,11 @@ func TestPrevUntil(t *testing.T) { assertSelectionIs(t, sel, "h4") } +func TestPrevUntilInvalid(t *testing.T) { + sel := Doc().Find(".alert p").PrevUntil("") + assertLength(t, sel.Nodes, 2) +} + func TestPrevUntil2(t *testing.T) { sel := Doc().Find("[ng-cloak]").PrevUntil(":not([ng-cloak])") assertLength(t, sel.Nodes, 1) @@ -500,6 +570,11 @@ func TestNextFilteredUntil(t *testing.T) { assertSelectionIs(t, sel, "#n3", "#n5", "#nf3", "#nf5") } +func TestNextFilteredUntilInvalid(t *testing.T) { + sel := Doc2().Find(".two").NextFilteredUntil("", "") + assertLength(t, sel.Nodes, 0) +} + func TestNextFilteredUntilRollback(t *testing.T) { sel := Doc2().Find(".two") sel2 := sel.NextFilteredUntil(".even", ".six").End() @@ -542,6 +617,11 @@ func TestPrevFilteredUntil(t *testing.T) { assertSelectionIs(t, sel, "#n4", "#n2", "#nf4", "#nf2") } +func TestPrevFilteredUntilInvalid(t *testing.T) { + sel := Doc2().Find(".five").PrevFilteredUntil("", "") + assertLength(t, sel.Nodes, 0) +} + func TestPrevFilteredUntilRollback(t *testing.T) { sel := Doc2().Find(".four") sel2 := sel.PrevFilteredUntil(".odd", ".one").End() @@ -598,6 +678,12 @@ func TestClosestNone(t *testing.T) { assertLength(t, sel2.Nodes, 0) } +func TestClosestInvalid(t *testing.T) { + sel := Doc().Find("h4") + sel2 := sel.Closest("") + assertLength(t, sel2.Nodes, 0) +} + func TestClosestMany(t *testing.T) { sel := Doc().Find(".container-fluid") sel2 := sel.Closest(".pvk-content") diff --git a/type.go b/type.go index 2f87120..e2169fa 100644 --- a/type.go +++ b/type.go @@ -6,6 +6,8 @@ import ( "net/http" "net/url" + "github.com/andybalholm/cascadia" + "golang.org/x/net/html" ) @@ -113,3 +115,21 @@ type Matcher interface { MatchAll(*html.Node) []*html.Node Filter([]*html.Node) []*html.Node } + +// compileMatcher compiles the selector string s and returns +// the corresponding Matcher. If s is an invalid selector string, +// it returns a Matcher that fails all matches. +func compileMatcher(s string) Matcher { + cs, err := cascadia.Compile(s) + if err != nil { + return invalidMatcher{} + } + return cs +} + +// invalidMatcher is a Matcher that always fails to match. +type invalidMatcher struct{} + +func (invalidMatcher) Match(n *html.Node) bool { return false } +func (invalidMatcher) MatchAll(n *html.Node) []*html.Node { return nil } +func (invalidMatcher) Filter(ns []*html.Node) []*html.Node { return nil } diff --git a/type_test.go b/type_test.go index ae045fe..83d26ae 100644 --- a/type_test.go +++ b/type_test.go @@ -108,6 +108,19 @@ func printSel(t *testing.T, sel *Selection) { } } +func shortPrintSel(t *testing.T, sel *Selection) { + sel.Each(func(i int, s *Selection) { + h, _ := OuterHtml(s) + if ix := strings.Index(h, "\n"); ix >= 0 { + h = h[:ix] + } + if len(h) > 100 { + h = h[:100] + } + fmt.Println(i, ">>> ", h) + }) +} + func loadDoc(page string) *Document { var f *os.File var e error