Compare commits

...
359 Commits
Author SHA1 Message Date
Martin Angers 4a4f1326fa Linkify github usernames 2026-08-27 09:17:53 -04:00
Martin Angers e47f3848de Add new version to readme to prepare 1.13 release 2026-08-27 09:15:43 -04:00
Martin Angers 80a9b7fb55 Run CI on go1.27 (+1.26) 2026-08-27 09:09:59 -04:00
Martin AngersandGitHub ad18e5a957 Merge pull request #596 from ChrisJr404/text-toplevel-func
Add top-level Text function for BeautifulSoup-style text extraction
2026-08-27 09:08:42 -04:00
Chris (ChrisJr404) 4e49d09e18 add tests for empty and fully-filtered selections 2026-08-24 14:00:20 -04:00
Chris (ChrisJr404) 91ed06d7c5 Add top-level Text function for BeautifulSoup-style text extraction
Adds goquery.Text(s, *TextOptions), a package-level counterpart to the
Selection.Text method that gives control over how the text of distinct
text nodes is joined (Separator), whether each node is trimmed of
surrounding whitespace (Trim), and which text nodes are included (Keep).
This covers the common need to extract clean, readable text from a
document - for example joining fragments with a space and dropping the
text of script/style elements - without hand-rolling a node walk.

Passing a nil TextOptions keeps the behaviour identical to Selection.Text.
Includes tests and a runnable example.
2026-08-18 08:54:59 -04:00
Martin AngersandGitHub 105f3990ac Merge pull request #595 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.58.0
build(deps): bump golang.org/x/net from 0.57.0 to 0.58.0
2026-08-17 09:17:03 -04:00
dependabot[bot]andGitHub d329e294ad build(deps): bump golang.org/x/net from 0.57.0 to 0.58.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.57.0 to 0.58.0.
- [Commits](https://github.com/golang/net/compare/v0.57.0...v0.58.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.58.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 05:52:35 +00:00
Martin AngersandGitHub 53a9b37ea8 Merge pull request #586 from jvoisin/cachephwc
cleanup: extract cachedParse helper for parse-result reuse
2026-07-31 10:27:25 -04:00
Martin AngersandGitHub 50e9856dda Merge pull request #593 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.57.0
build(deps): bump golang.org/x/net from 0.56.0 to 0.57.0
2026-07-17 16:17:28 -04:00
Martin AngersandGitHub 3a8dfd5b5f Merge pull request #592 from PuerkitoBio/dependabot/github_actions/actions/checkout-7
build(deps): bump actions/checkout from 6 to 7
2026-07-16 13:44:18 -04:00
dependabot[bot]andGitHub a92dca932b build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 17:32:11 +00:00
Martin AngersandGitHub 91fa5c4527 Merge pull request #594 from PuerkitoBio/dependabot/github_actions/actions/setup-go-7
build(deps): bump actions/setup-go from 6 to 7
2026-07-16 13:30:13 -04:00
dependabot[bot]andGitHub a72ee866ea build(deps): bump actions/setup-go from 6 to 7
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 05:52:38 +00:00
dependabot[bot]andGitHub 5b72b928dc build(deps): bump golang.org/x/net from 0.56.0 to 0.57.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0.
- [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-09 05:52:36 +00:00
Martin AngersandGitHub a81d5bb195 Merge pull request #590 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.56.0
build(deps): bump golang.org/x/net from 0.55.0 to 0.56.0
2026-06-11 08:58:19 -04:00
dependabot[bot]andGitHub 5facb40e7c build(deps): bump golang.org/x/net from 0.55.0 to 0.56.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0.
- [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-11 05:52:36 +00:00
Martin AngersandGitHub 2bab3fcead Merge pull request #589 from jvoisin/attror
cleanup: inline getAttributeValue into Attr, delegate AttrOr
2026-06-10 09:00:51 -04:00
Martin AngersandGitHub ad7e3a5a32 Merge pull request #588 from jvoisin/deadparam
refactor: remove a dead param in getClassesAndAttr
2026-06-10 08:58:14 -04:00
Martin AngersandGitHub e66583975d Merge pull request #585 from jvoisin/winnow_cleanup
cleanup: thread []*html.Node through winnow, drop temp Selection
2026-06-10 08:56:49 -04:00
Martin AngersandGitHub dfcd45f03f Merge pull request #584 from jvoisin/phwc
cleanup: factor parseHtml through parseHtmlWithContext
2026-06-10 08:53:27 -04:00
Martin AngersandGitHub 6a3f1a4508 Merge pull request #583 from jvoisin/nocon
cleanup: drop redundant FirstChild nil-check in textHelper
2026-06-10 08:51:31 -04:00
jvoisin 3d73db28d1 cleanup: inline getAttributeValue into Attr, delegate AttrOr
Attr was a 4-line wrapper around getAttributeValue, which itself was
a 6-line wrapper around getAttributePtr.

This commit inlines getAttributeValue into Attr and
have AttrOr delegate to Attr. Drops a layer of indirection and one
helper function; both methods get shorter and the empty-selection
check lives in one place.
2026-06-05 23:48:33 +02:00
jvoisin ab25b2a2f9 refactor: remove a dead param in getClassesAndAttr
Every caller of getClassesAndAttr is passing `true` as second parameter, so
let's get rid of it.
2026-06-05 23:23:26 +02:00
jvoisin a7a9c2dced cleanup: extract cachedParse helper for parse-result reuse
WrapHtml, WrapInnerHtml, and eachNodeHtml each open-coded the same
lookup-or-parse-and-store sequence against a map[string][]*html.Node
keyed by the context node's name. Extract that 4-line sequence into
a tiny cachedParseHtmlWithContext(cache, htmlStr, context) helper; each call
site becomes a linear, closure-free loop and the only duplicated bit (the cache
lookup) lives in one place.
2026-06-05 22:51:00 +02:00
jvoisin 36f7b3b68e cleanup: thread []*html.Node through winnow, drop temp Selection
winnow only ever reads sel.Nodes; the *Selection parameter was
incidental. Change its signature to winnow(nodes []*html.Node, ...)
and update its three callers (FilterMatcher, NotMatcher,
filterAndPush). filterAndPush no longer needs to fabricate a
throwaway Selection to satisfy the call.

While this change has o measurable runtime effect, it simplifies the code a bit,
by making winnow's signature clearer, and removes a now-useless line and its
associated comment in filterAndPush
2026-06-05 22:35:48 +02:00
jvoisin e96c2a87e6 cleanup: factor parseHtml through parseHtmlWithContext
parseHtml and parseHtmlWithContext duplicated the ParseFragment call,
the error check, and the panic message. parseHtml only differed by
passing a synthetic ElementNode as the context.

This commit simply delegates parseHtml to parseHtmlWithContext.
2026-06-05 22:29:08 +02:00
jvoisin a202b87b3b cleanup: drop redundant FirstChild nil-check in textHelper
The `for c := n.FirstChild; c != nil; c = c.NextSibling` loop already
handles the nil-FirstChild case.
2026-06-05 22:26:30 +02:00
Martin AngersandGitHub 88920eaad4 Merge pull request #569 from jvoisin/specialized
perf: probe with MatchFirst in HasMatcher to avoid slice allocation
2026-06-04 10:23:08 -04:00
Martin AngersandGitHub 65a48d3886 Merge pull request #575 from jvoisin/nodedup
test: cover children traversals with duplicate source nodes
2026-06-04 10:17:21 -04:00
Martin AngersandGitHub b8d1127ab1 Merge pull request #574 from jvoisin/preact
perf: presize the result slice in Empty()
2026-06-04 09:57:10 -04:00
Martin AngersandGitHub 939767f12c Merge pull request #582 from PuerkitoBio/dependabot/go_modules/github.com/andybalholm/cascadia-1.3.4
build(deps): bump github.com/andybalholm/cascadia from 1.3.3 to 1.3.4
2026-06-04 09:41:47 -04:00
dependabot[bot]andGitHub edf734c843 build(deps): bump github.com/andybalholm/cascadia from 1.3.3 to 1.3.4
Bumps [github.com/andybalholm/cascadia](https://github.com/andybalholm/cascadia) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/andybalholm/cascadia/releases)
- [Commits](https://github.com/andybalholm/cascadia/compare/v1.3.3...v1.3.4)

---
updated-dependencies:
- dependency-name: github.com/andybalholm/cascadia
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 22:34:05 +00:00
jvoisin df88dce547 Refactor first-match optimization into singleMatcher
Move the "stop at first match" optimization out of HasMatcher and into
singleMatcher, where it can be reused elsewhere.

singleMatcher now implements MatchFirst, which calls the underlying
Matcher's MatchFirst if it provides one (cascadia-compiled matchers all
do), otherwise falls back to MatchAll and returns its first element.
singleMatcher.MatchAll wraps MatchFirst's result in a single-item slice
to respect the Matcher interface, so external SingleMatcher() users get
the slice signature while internal callers can avoid that allocation.

HasMatcher now constructs a singleMatcher directly to call MatchFirst
without a type assertion, removing the duplicated probe branch.
2026-06-02 14:15:45 +02:00
Martin AngersandGitHub 2955b8bf81 Merge pull request #568 from jvoisin/grnkgnr
perf: collect parent nodes inline in getParentNodes
2026-06-01 11:35:56 -04:00
Martin AngersandGitHub 57abc1b153 Merge pull request #565 from jvoisin/prez
perf: presize set in appendWithoutDuplicates to final expected size
2026-06-01 11:32:54 -04:00
Martin AngersandGitHub feced993c3 Merge pull request #567 from jvoisin/precize
perf: presize result slice in getChildrenWithSiblingType
2026-06-01 11:10:34 -04:00
jvoisin cc35e5b0e5 test: cover children traversals with duplicate source nodes
A Selection's Nodes field is exported, so callers can build a selection that
holds the same node more than once. Add a regression test asserting that
Children, ChildrenFiltered and Contents deduplicate the shared child set
rather than returning it once per duplicate source node.
2026-06-01 09:11:37 +02:00
jvoisin ea09fe9e1c perf: presize the result slice in Empty()
Empty() collected the removed children into a nil slice grown via append,
forcing a series of grow-and-double reallocations proportional to the number
of children.

This commit walks the children once up front to count them, then allocate the
result slice at its final capacity before the removal loop.

The count pass is a cheap, branch-predicted pointer walk that replaces several
slice reallocations with a single allocation.

benchstat (-benchtime=2000x, count=10):

                |   old    |            new            |
                |  allocs  |  allocs    vs base        |
    Empty10-8   |   3.000  |  2.000     -33.33%        |
    Empty100-8  |   6.000  |  2.000     -66.67%        |
    Empty1000-8 |   9.000  |  2.000     -77.78%        |

    B/op:    -47% / -56% / -53%
    sec/op:  Empty10 -53%, Empty100 -58%, Empty1000 ~ (noise)
2026-06-01 01:23:40 +02:00
jvoisin 021fb11910 perf: probe with MatchFirst in HasMatcher to avoid slice allocation
HasMatcher only needs to know whether a child subtree contains a match,
but it tested len(m.MatchAll(c)) > 0 after wrapping the matcher with
SingleMatcher. SingleMatcher.MatchAll computes MatchFirst internally and
then wraps the result in a throwaway []*html.Node{node}, allocating a
one-element slice per matching child subtree just to check a boolean.

When the matcher exposes MatchFirst, probe it directly and test the returned
node against nil, avoiding the allocation entirely. This is the same trick
already used in SingleMatcher.MatchAll, just applied at the HasMatcher call
site so the intermediate slice is never built in the first place. The
SingleMatcher path is kept as a fallback for matchers that don't implement
MatchFirst.

benchstat (count=10):

      | before       |              after   |
      | sec/op       |    sec/op     vs base|
Has-8 | 2.675µ ± 17% | 2.208µ ± 17%  -17.47%|

      | before       |             after    |
      |  B/op        |    B/op     vs base  |
Has-8 | 360.0 ± 0%   | 240.0 ± 0%  -33.33%  |

      | before       |             after    |
      |allocs/op     | allocs/op   vs base  |
Has-8 | 20.00 ± 0%   |  6.000 ± 0%  -70.00% |
2026-05-30 23:18:55 +02:00
jvoisin 116f677017 perf: collect parent nodes inline in getParentNodes
getParentNodes ran through mapNodes with a callback that allocated a
throwaway one-element []*html.Node{n.Parent} for every source node. Since
sibling nodes share the same parent, almost all of those slices were
immediately discarded as duplicates.

This commit collects the parents directly in a single loop, deduplicating via a
set as we go, so no per-node slice is allocated.

benchstat (count=10, all p=0.000):

                 |   before    |             after            |
                 |   sec/op    |    sec/op     vs base        |
Parent-8         | 38.54µ ± 21%|  17.49µ ± 14%  -54.63%       |
ParentFiltered-8 | 46.57µ ± 13%|  20.91µ ± 24%  -55.09%       |

                 |   before    |             after            |
                 |    B/op     |     B/op      vs base        |
Parent-8         | 13.24Ki ± 0%|  10.28Ki ± 0%  -22.36%       |
ParentFiltered-8 | 13.49Ki ± 0%|  10.58Ki ± 0%  -21.60%       |

                 |   before    |            after             |
                 |  allocs/op  | allocs/op   vs base          |
Parent-8         |  384.0 ± 0% | 10.00 ± 0%  -97.40%          |
ParentFiltered-8 |  392.0 ± 0% | 19.00 ± 0%  -95.15%          |
2026-05-30 22:52:34 +02:00
jvoisin f4b3a3e2c4 perf: presize result slice in getChildrenWithSiblingType
For the sibling types that collect every match (siblingAll,
siblingAllIncludingNonElements, siblingPrevAll, siblingNextAll), do a
cheap first-pass pointer walk to count the matches, then make the result
slice with that exact capacity. This removes the repeated append-driven
slice growth that dominated allocations on these hot traversal paths.

The Until cases are left untouched because counting would require running
the user predicate twice, and the single-result Next/Prev cases have
nothing to presize.

This drives Children/Contents/Siblings/Next*/Prev* through fewer
allocations:

                       allocs/op vs base    B/op vs base
    Siblings            -57.23%             -9.23%
    SiblingsFiltered    -54.49%             -8.94%
    NextAll             -59.82%            -10.04%
    NextAllFiltered     -55.83%             -9.65%
    PrevAll             -55.32%            -10.66%
    PrevAllFiltered     -52.53%            -10.40%
    ChildrenFiltered    -15.38%             -6.45%
    Contents             -7.69%             -1.73%
    geomean             -25.07%             -3.88%

The Until and single Next/Prev benchmarks are unchanged (bit-identical
allocs/op), confirming the count pre-pass only touches the collect-all
paths.

The quite verbose comment on top of the change is there so that future
generations won't waste time wondering why this weird loop is here.
2026-05-29 23:45:29 +02:00
jvoisin 9285d9bf28 perf: presize set in appendWithoutDuplicates to final expected size
BenchmarkAddNodesBig (DocW().Find("li") duplicated to ~1500 nodes,
then AddNodes onto an empty selection), -count=20:

                 sec/op             B/op           allocs/op
    before    214.3µs ±12%     27.58Ki ±0%       24
    after     104.8µs ±24%     45.24Ki ±0%       16
                  -51%             +64%          -33%

The B/op increase is the cost of presizing the map to its final
capacity in one shot rather than letting the runtime grow it
through smaller bucket arrays (which are freed but counted in
total bytes allocated). Peak resident memory is comparable.
2026-05-29 22:02:39 +02:00
Martin AngersandGitHub fa0370e85c Merge pull request #564 from jvoisin/hasm
perf: probe subtrees directly in Has / HasMatcher
2026-05-29 13:17:41 -04:00
jvoisin 9082a116aa perf: probe subtrees directly in Has / HasMatcher
Has(selector) and HasMatcher(m) used to call
s.document.Find(selector)/FindMatcher(m), materializing every matching
descendant in the entire document, then iterate s.Nodes x matchedNodes x
treeDepth via nodeContains to keep the elements that contain one.

This commit probes each selection node's subtree directly with SingleMatcher, which
short-circuits on the first descendant match.

BenchmarkHas (DocW().Find("h2").Has(".editsection")), -count=10:

               sec/op       B/op
    before     61.97µs       744 B
    after       3.33µs       360 B
                -94.6%      -51.6%
2026-05-29 17:19:03 +02:00
Martin AngersandGitHub 7a46fb40e9 Merge pull request #563 from jvoisin/precise
perf: presize node-set maps in mapNodes, ClosestNodes and winnowNodes
2026-05-28 10:31:02 -04:00
Martin AngersandGitHub bd4f51414c Merge branch 'master' into precise 2026-05-28 10:21:55 -04:00
Martin AngersandGitHub 59eb2061a0 Merge pull request #561 from jvoisin/noclosure
perf: speed up Selection.Text by outlining recursion
2026-05-28 10:19:23 -04:00
Martin AngersandGitHub 7f540a45a4 Merge pull request #560 from jvoisin/if
perf: pre-check raw class attribute in HasClass
2026-05-28 10:17:55 -04:00
Martin AngersandGitHub d260d2936f Merge pull request #558 from jvoisin/has-nodes-direct
perf: walk s.Nodes directly in HasNodes
2026-05-28 10:15:26 -04:00
Martin AngersandGitHub b2b7542bc5 Merge pull request #557 from jvoisin/winnow-nodes-direct
perf: drop grep/closure indirection in winnowNodes
2026-05-28 10:09:16 -04:00
Martin AngersandGitHub c38303d8bf Merge pull request #550 from jvoisin/nomap
perf: skip dedup map in mapNodes for single-node selections
2026-05-28 10:05:55 -04:00
Martin AngersandGitHub 32e5d34ae4 Merge pull request #547 from jvoisin/idx
perf: replace Index() PrevAll traversal with direct sibling walk
2026-05-28 10:03:04 -04:00
Martin AngersandGitHub 69b9b29a1f Merge pull request #559 from jvoisin/is-matcher-early-return
perf: short-circuit IsMatcher on the first match
2026-05-28 09:52:50 -04:00
jvoisin 5f07eebb27 perf: presize node-set maps in mapNodes, ClosestNodes and winnowNodes
Three internal helpers build a map[*html.Node]bool from a known set of
nodes (or use one as a dedup accumulator over a known number of input
nodes) but allocate the map without a size hint. They then incur
grow-and-double rehashing as entries are inserted.

Pass len(nodes) as the size hint at make time:

- traversal.go mapNodes: dedup map shared by appendWithoutDuplicates
  across every per-node result. mapNodes is the engine behind most
  traversal helpers (Parents, Children, Next/Prev, Siblings, Find*,
  Contents, ...), so the impact is broad.
- traversal.go ClosestNodes: target-node set built from nodes...
- filter.go winnowNodes: large-N path's lookup set built from nodes...

Double-digit performance gains across ~all benchmarks.
2026-05-27 23:11:36 +02:00
jvoisin 25b585d336 perf: skip dedup map in mapNodes for single-node selections
mapNodes always allocated a map[*html.Node]bool for deduplication, even
when called with a single source node where duplicates cannot arise.
Short-circuit to return the callback result directly when len(nodes)==1.
Also add a special case for when there are zero nodes.

This benefits all traversal methods (Find, Children, Parent, Next, Prev,
etc.) when operating on a single-node selection, which is the common
case after First(), Eq(), or directly on a Document.

name      old ns/op   new ns/op   delta
Find-8    23200       14200       -38.8%

name      old B/op    new B/op    delta
Find-8    4632        1496        -67.7%

name      old allocs  new allocs  delta
Find-8    27          13          -51.9%
2026-05-27 21:17:38 +02:00
jvoisin ece69fc2fd perf: speed up Selection.Text by outlining recursion
Replace the recursive closure in Selection.Text with a helper method call.
This keeps behavior unchanged while reducing runtime overhead in the hot path.

Benchmark (BenchmarkText, benchstat, n=12):
- sec/op: 2.133us -> 1.692us (-20.70%, p=0.000)
- B/op: 504 -> 504 (no change)
- allocs/op: 6 -> 6 (no change)
2026-05-27 21:12:36 +02:00
jvoisin 3b71495757 perf: pre-check raw class attribute in HasClass
HasClass normalized every matching element's class attribute with
classTrimReplacer before checking for the target token, even when the
raw attribute value could not possibly contain that class at all.

Add a cheap strings.Contains(attr.Val, rawClass) pre-check and only run
the replacer on plausible matches. This preserves behavior for class
attributes containing tabs/newlines while skipping most of the work on
misses.

BenchmarkHasClass on arm64 over 10 runs:

name         old time/op   new time/op   delta
HasClass-8    9.809µs       2.660µs      -72.89% (p=0.000)
2026-05-27 15:26:22 +02:00
jvoisin 535ec7c4f2 perf: short-circuit IsMatcher on the first match
IsMatcher handled multi-node selections by calling m.Filter(s.Nodes) and
checking whether the returned slice was non-empty, forcing the matcher to walk
the whole selection and materialize all matches even though the API only needs
an existence check.

This commit replaces this with a simple loop over s.Nodes that calls m.Match(n)
and returns true on the first match. This also makes the single-node special
case unnecessary.

Benchmark results on arm64 over 10 runs:

name            old time/op   new time/op   delta
Is-8             4.67µs        1.32µs       -71.7%
IsPositional-8   21.44µs       0.82µs       -96.2%

name            old B/op      new B/op      delta
Is-8             96           80           -16.7%
IsPositional-8   1144         184          -83.9%

name            old allocs    new allocs    delta
Is-8             5            4            -20.0%
IsPositional-8   11           7            -36.4%
2026-05-27 15:19:58 +02:00
jvoisin bfc9c5940d perf: walk s.Nodes directly in HasNodes
HasNodes routed through FilterFunction -> winnowFunction -> grep, which
allocated a single-node Selection and a closure capture per source element,
just to call sel.Contains(n) which is itself a one-iteration sliceContains over
the wrapped node. This commit inlines a direct double loop over s.Nodes x nodes
calling nodeContains, and push the result via pushStack.

name             old allocs/op  new allocs/op  delta
Has-8                  55.00 ± 0%     24.00 ± 0%  -56.36% (p=0.000)
HasNodes-8            752.00 ± 0%      4.00 ± 0%  -99.47% (p=0.000)
HasSelection-8        752.00 ± 0%      3.00 ± 0%  -99.60% (p=0.000)
2026-05-27 12:11:29 +02:00
jvoisin 8ea13ea017 perf: drop grep/closure indirection in winnowNodes
In the same spirit as #545, replaces costly grep() with a loop.

While escape analysis managed to stack-allocate the per-node
Selection through grep's inlining, the closure capture itself (nodes and keep)
was still heap-allocated. Local benchmarks are showing a 30% allocs/op
reduction, and a ~10-20% reduction in sec/op.
2026-05-27 11:52:43 +02:00
Martin AngersandGitHub e2468fa06d Merge pull request #552 from jvoisin/spaceclass
perf: replace regex+Split with strings.Fields in getClassesSlice
2026-05-26 09:12:25 -04:00
Martin AngersandGitHub 5350c00a95 Merge branch 'master' into spaceclass 2026-05-26 09:11:05 -04:00
Martin AngersandGitHub af20c4b0a0 Merge pull request #553 from jvoisin/slyce
perf: call Match/isInSlice directly in Until traversal helpers
2026-05-26 09:00:57 -04:00
Martin AngersandGitHub b27992034a Merge pull request #551 from jvoisin/nore
perf: replace rxClassTrim regexp with strings.Replacer
2026-05-26 08:59:19 -04:00
Martin AngersandGitHub 67dec88260 Merge pull request #548 from jvoisin/shortcircuit
perf: short-circuit IsFunction, IsSelection, and IsNodes
2026-05-26 08:53:05 -04:00
Martin AngersandGitHub cb1755d318 Merge pull request #554 from jvoisin/gpn
perf: drop per-parent Selection allocation in getParentsNodes
2026-05-26 08:49:01 -04:00
jvoisin 5d01e23f35 perf: drop per-parent Selection allocation in getParentsNodes
Mirrors #553 for getSiblingNodes, now extended to the ancestor traversal path.
2026-05-25 20:52:51 +02:00
jvoisin b7030c526f perf: call Match/isInSlice directly in Until traversal helpers
getParentsNodes and getSiblingNodes created a newSingleSelection per
ancestor/sibling node just to call IsMatcher or IsNodes for the
until-stop condition. Since these are always single-node checks, call
stopm.Match(p) and isInSlice(stopNodes, p) directly, avoiding the
Selection allocation and the method dispatch overhead.

name                      old ns/op   new ns/op   delta
ParentsUntil-8            30700       16100       -47.6%
ParentsUntilNodes-8       82700       28400       -65.7%
NextUntil-8               38000       25700       -32.4%
NextUntilNodes-8          13700       5900        -56.9%
PrevUntil-8               109600      82600       -24.6%
PrevUntilNodes-8          11600       4600        -60.3%

name                      old B/op    new B/op    delta
ParentsUntil-8            11528       4248        -63.2%
ParentsUntilNodes-8       39040       10448       -73.2%
NextUntil-8               8592        7712        -10.2%
NextUntilNodes-8          5536        1304        -76.4%
PrevUntil-8               30344       28280       -6.8%
PrevUntilNodes-8          4248        920         -78.3%

name                      old allocs  new allocs  delta
ParentsUntil-8            304         44          -85.5%
ParentsUntilNodes-8       904         60          -93.4%
NextUntil-8               216         106         -50.9%
NextUntilNodes-8          156         27          -82.7%
PrevUntil-8               408         150         -63.2%
PrevUntilNodes-8          129         23          -82.2%
2026-05-24 22:48:16 +02:00
jvoisin c19544b145 perf: replace regex+Split with strings.Fields in getClassesSlice
getClassesSlice used rxClassTrim.ReplaceAllString followed by
strings.Split, which allocated twice and produced empty strings from
leading/trailing spaces. strings.Fields handles all Unicode whitespace
in a single pass with no empty entries.

name             old ns/op   new ns/op   delta
AddClass-8       176700      107500      -39.2%
RemoveClass-8    142900      64600       -54.8%

name             old B/op    new B/op    delta
AddClass-8       12544       7024        -44.0%
RemoveClass-8    6503        1777        -72.7%

name             old allocs  new allocs  delta
AddClass-8       697         374         -46.3%
RemoveClass-8    697         374         -46.3%
2026-05-24 22:38:53 +02:00
jvoisin 41a5c22149 perf: replace rxClassTrim regexp with strings.Replacer
The regex [\t\r\n] only does single-byte substitution, which is exactly
what strings.Replacer's byteReplacer is optimized for. This removes the
regexp dependency from property.go and drops the redundant ContainsAny
fast-path guard in HasClass, since byteReplacer already returns the
original string unchanged (no allocation) when nothing matches.

Benchmark shows ~18% improvement on HasClass with 0 allocs either way.
2026-05-24 22:30:18 +02:00
Martin AngersandGitHub 83a0a8bbd8 Merge pull request #545 from jvoisin/nog
perf: avoid per-node Selection allocation in winnow Not path
2026-05-24 10:19:43 -04:00
Martin AngersandGitHub cb2cc9d25e Merge pull request #549 from jvoisin/lack_of_class
perf: skip regex in getClassesAndAttr when no \t\r\n present
2026-05-24 10:07:42 -04:00
jvoisin 3c647f82fe perf: skip regex in getClassesAndAttr when no \t\r\n present
getClassesAndAttr unconditionally ran rxClassTrim.ReplaceAllString to
normalize tabs, carriage returns, and newlines in class attribute values.
The vast majority of HTML class attributes contain none of these, so add
a strings.ContainsAny fast path that falls back to simple concatenation.

This benefits all callers: HasClass, AddClass, RemoveClass, ToggleClass. Here's
a benchmark of HasClass to illustrate the gains:

name          old ns/op   new ns/op   delta
HasClass-8    222000      45300       -79.6%

name          old B/op    new B/op    delta
HasClass-8    19152       4280        -77.6%

name          old allocs  new allocs  delta
HasClass-8    1300        325         -75.0%
2026-05-23 23:11:00 +02:00
jvoisin d94aeb90e8 perf: short-circuit IsFunction, IsSelection, and IsNodes
These methods were building a full filtered Selection (via
FilterFunction/FilterSelection/FilterNodes -> winnow -> grep) only to
check Length() > 0. This commit replaces with early-return loops that stop at
the first match, significantly speeding them up:

name             old ns/op   new ns/op   delta
IsFunction-8     1530        3.9         -99.7%
IsSelection-8    1890        19.9        -98.9%
IsNodes-8        2016        19.9        -99.0%

name             old B/op    new B/op    delta
IsFunction-8     784         0           -100.0%
IsSelection-8    72          0           -100.0%
IsNodes-8        72          0           -100.0%

name             old allocs  new allocs  delta
IsFunction-8     28          0           -100.0%
IsSelection-8    3           0           -100.0%
IsNodes-8        3           0           -100.0%
2026-05-23 22:57:17 +02:00
jvoisin 5f0afd53e4 perf: replace Index() PrevAll traversal with direct sibling walk
Index() was creating a Selection, calling PrevAll() which traversed
through getSiblingNodes -> mapNodes (allocating a dedup map and
intermediate slices), then calling Length() on the result.

This commit replaces this with a direct PrevSibling pointer walk that counts
element nodes, bringing the number of allocations down to zero instead of a
couple of hundreds on local benchmarks
2026-05-23 22:50:53 +02:00
jvoisin 45d35d506a perf: avoid per-node Selection allocation in winnow Not path
The winnow() "Not" path routed through grep(), which called
newSingleSelection() for every node, allocating a Selection and
a 1-element []*html.Node slice on the heap per node, only to
immediately unwrap it via s.Get(0) for m.Match().
This commit call m.Match(n) directly and pre-allocate the result slice.

It also adds BenchmarkNotMatcher to isolate the winnow path from
compileMatcher overhead.

Here are my local benchmark results, for BenchmarkNotMatcher with 373 <li>
nodes, and a pre-compiled matcher:

old ns/op   new ns/op   delta
22750       19350       ~-15%

old B/op    new B/op    delta
9384        3120        -66.7%

old allocs  new allocs  delta
11          2           -81.8%
2026-05-23 22:02:15 +02:00
Martin AngersandGitHub 33466f5bcd Merge pull request #544 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.55.0
Bump golang.org/x/net from 0.54.0 to 0.55.0
2026-05-22 08:52:45 -04:00
dependabot[bot]andGitHub 33ce051cd9 Bump golang.org/x/net from 0.54.0 to 0.55.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 05:52:54 +00:00
Martin AngersandGitHub 8c2d01c469 Merge pull request #543 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.54.0
Bump golang.org/x/net from 0.53.0 to 0.54.0
2026-05-13 21:34:12 -04:00
dependabot[bot]andGitHub 21b9b64897 Bump golang.org/x/net from 0.53.0 to 0.54.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0.
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 06:41:06 +00:00
Martin AngersandGitHub c92c45163d Merge pull request #542 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.53.0
Bump golang.org/x/net from 0.52.0 to 0.53.0
2026-04-11 09:47:15 -04:00
dependabot[bot]andGitHub 43d03d8533 Bump golang.org/x/net from 0.52.0 to 0.53.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.52.0 to 0.53.0.
- [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 05:52:49 +00:00
Martin Angers 401642b253 Update readme to prepare for 1.12 release 2026-03-15 12:26:31 -04:00
Martin AngersandGitHub afd9326761 Merge pull request #540 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.52.0 2026-03-15 12:25:04 -04:00
dependabot[bot]andGitHub f799f78b6e Bump golang.org/x/net from 0.50.0 to 0.52.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.52.0.
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-15 16:21:07 +00:00
Martin Angers 805d7b1d5c Update CI to 1.25 and 1.26 2026-03-15 12:19:53 -04:00
Martin AngersandGitHub 7dc7e6463b Merge pull request #538 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.50.0 2026-02-13 10:10:41 -05:00
dependabot[bot]andGitHub 3020ebb46d Bump golang.org/x/net from 0.49.0 to 0.50.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 05:52:35 +00:00
Martin AngersandGitHub ee4bcdb889 Merge pull request #537 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.49.0
Bump golang.org/x/net from 0.48.0 to 0.49.0
2026-01-17 10:29:26 -05:00
dependabot[bot]andGitHub f3bc303dcb Bump golang.org/x/net from 0.48.0 to 0.49.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 07:22:25 +00:00
Martin AngersandGitHub 0f22828bca Merge pull request #536 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.48.0
Bump golang.org/x/net from 0.47.0 to 0.48.0
2025-12-23 09:13:59 -05:00
dependabot[bot]andGitHub 68de6a15bb Bump golang.org/x/net from 0.47.0 to 0.48.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.48.0.
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 05:05:12 +00:00
Martin AngersandGitHub 8ef6c9c294 Merge pull request #535 from PuerkitoBio/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-21 16:01:03 -05:00
dependabot[bot]andGitHub ffba93fcef Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 05:20:57 +00:00
Martin AngersandGitHub d3e3ce5663 Merge pull request #534 from PuerkitoBio/mna-prepare-1.11
Update readme for 1.11 release
2025-11-16 11:05:17 -05:00
Martin Angers 9841a36e8f Update readme for 1.11 release 2025-11-16 11:04:18 -05:00
Martin AngersandGitHub 469d4eab16 Merge pull request #533 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.47.0
Bump golang.org/x/net from 0.43.0 to 0.47.0
2025-11-16 11:00:17 -05:00
dependabot[bot]andGitHub a03654e5c2 Bump golang.org/x/net from 0.43.0 to 0.47.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.43.0 to 0.47.0.
- [Commits](https://github.com/golang/net/compare/v0.43.0...v0.47.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-16 15:58:30 +00:00
Martin AngersandGitHub faf16ca2df Merge pull request #523 from PuerkitoBio/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-11-16 10:56:22 -05:00
Martin Angers 4c1c718c62 Update test matrix to run on Go 1.24 and 1.25 2025-11-16 10:54:17 -05:00
dependabot[bot]andGitHub 62bfb673c9 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 14:08:26 +00:00
Martin AngersandGitHub e3142812ec Merge pull request #525 from PuerkitoBio/dependabot/github_actions/actions/setup-go-6
Bump actions/setup-go from 5 to 6
2025-09-04 10:07:26 -04:00
dependabot[bot]andGitHub 0961921b55 Bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 11:22:17 +00:00
Martin AngersandGitHub 695fbfd142 Merge pull request #522 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.43.0
Bump golang.org/x/net from 0.42.0 to 0.43.0
2025-08-08 11:27:15 -04:00
dependabot[bot]andGitHub 91702e55ec Bump golang.org/x/net from 0.42.0 to 0.43.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.42.0 to 0.43.0.
- [Commits](https://github.com/golang/net/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-08 05:10:58 +00:00
Martin AngersandGitHub 37bfcfb2ff Merge pull request #520 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.42.0
Bump golang.org/x/net from 0.41.0 to 0.42.0
2025-07-11 09:55:20 -04:00
dependabot[bot]andGitHub 2213b25d2b Bump golang.org/x/net from 0.41.0 to 0.42.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.41.0 to 0.42.0.
- [Commits](https://github.com/golang/net/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-11 05:32:37 +00:00
Martin AngersandGitHub 26759cf6df Merge pull request #519 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.41.0 2025-06-07 10:51:20 -04:00
dependabot[bot]andGitHub 01cff33869 Bump golang.org/x/net from 0.40.0 to 0.41.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 05:24:15 +00:00
Martin AngersandGitHub 250e47114e Merge pull request #518 from apocelipes/stringsreplaceall
Use strings.ReplaceAll to silence golangci-lint warnings
2025-05-23 09:47:45 -04:00
apocelipes 9ec8fd3fbf Use strings.ReplaceAll to silence golangci-lint warnings 2025-05-23 18:54:39 +08:00
Martin AngersandGitHub 32ce829020 Merge pull request #517 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.40.0
Bump golang.org/x/net from 0.39.0 to 0.40.0
2025-05-08 09:43:27 -04:00
dependabot[bot]andGitHub 8f996d0795 Bump golang.org/x/net from 0.39.0 to 0.40.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/net/compare/v0.39.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-06 05:35:57 +00:00
Martin Angers 9c6a69cd4d Prepare for v1.10.3 2025-04-11 14:15:50 -04:00
Martin AngersandGitHub 80f14c7111 Merge pull request #516 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.39.0
Bump golang.org/x/net from 0.38.0 to 0.39.0
2025-04-10 09:48:46 -04:00
dependabot[bot]andGitHub 58dcec12a0 Bump golang.org/x/net from 0.38.0 to 0.39.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/net/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 05:08:10 +00:00
Martin AngersandGitHub aadb1cdadc Merge pull request #515 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.38.0
Bump golang.org/x/net from 0.37.0 to 0.38.0
2025-03-28 12:15:05 -04:00
dependabot[bot]andGitHub 9cd3cf0815 Bump golang.org/x/net from 0.37.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 05:59:18 +00:00
Martin AngersandGitHub fa93633a0f Merge pull request #514 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.37.0 2025-03-05 21:35:54 -05:00
Martin Angers 858507ff1b Run go mod tidy 2025-03-05 21:32:02 -05:00
dependabot[bot]andGitHub 5c48549c05 Bump golang.org/x/net from 0.35.0 to 0.37.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.37.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 02:27:30 +00:00
Martin AngersandGitHub c2d1a0902f Merge pull request #512 from myxzlpltk/master
increase performance by migrating from bytes.Buffer to strings.Builder
2025-02-27 21:31:04 -05:00
Saddam Azy 3951e60495 increase performance by migrating from bytes.Buffer to strings.Builder 2025-02-27 08:48:50 +07:00
Martin Angers e8b5d743a8 Prepare for 1.10.2 2025-02-13 09:41:22 -05:00
Martin Angers a2ae60f048 Add go1.24 to test matrix 2025-02-13 09:35:55 -05:00
Martin AngersandGitHub 6d0ed2a352 Merge pull request #510 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.35.0
Bump golang.org/x/net from 0.34.0 to 0.35.0
2025-02-13 09:34:32 -05:00
dependabot[bot]andGitHub 86e3da86b2 Bump golang.org/x/net from 0.34.0 to 0.35.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.34.0 to 0.35.0.
- [Commits](https://github.com/golang/net/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-11 05:55:08 +00:00
Martin AngersandGitHub 4128600425 Merge pull request #505 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.34.0
Bump golang.org/x/net from 0.33.0 to 0.34.0
2025-01-11 11:35:10 -05:00
dependabot[bot]andGitHub 3db152488a Bump golang.org/x/net from 0.33.0 to 0.34.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-07 05:25:05 +00:00
Martin AngersandGitHub 1da3b302d3 Merge pull request #503 from iamleot/prepare-for-v1.10.1 2024-12-26 17:12:12 -05:00
Leonardo Taccari 985aab32ba Prepare for version v1.10.1
Add changelog entry for 1.10.1.

Only dependencies update, however includes fixes for GO-2024-3333.
2024-12-26 17:39:06 +01:00
Martin AngersandGitHub b995434ce7 Merge pull request #502 from PuerkitoBio/dependabot/go_modules/github.com/andybalholm/cascadia-1.3.3
Bump github.com/andybalholm/cascadia from 1.3.2 to 1.3.3
2024-12-20 09:54:21 -05:00
dependabot[bot]andGitHub 381d6b903d Bump github.com/andybalholm/cascadia from 1.3.2 to 1.3.3
Bumps [github.com/andybalholm/cascadia](https://github.com/andybalholm/cascadia) from 1.3.2 to 1.3.3.
- [Release notes](https://github.com/andybalholm/cascadia/releases)
- [Commits](https://github.com/andybalholm/cascadia/compare/v1.3.2...v1.3.3)

---
updated-dependencies:
- dependency-name: github.com/andybalholm/cascadia
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 05:44:06 +00:00
Martin AngersandGitHub 69f0ce22d3 Merge pull request #501 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.33.0
Bump golang.org/x/net from 0.32.0 to 0.33.0
2024-12-19 10:54:47 -05:00
dependabot[bot]andGitHub 40758683ed Bump golang.org/x/net from 0.32.0 to 0.33.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-19 05:47:33 +00:00
Martin AngersandGitHub 13c6a0c3d9 Merge pull request #499 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.32.0
Bump golang.org/x/net from 0.31.0 to 0.32.0
2024-12-05 08:57:55 -05:00
dependabot[bot]andGitHub 0f6e1ada86 Bump golang.org/x/net from 0.31.0 to 0.32.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/net/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-05 05:10:44 +00:00
Martin AngersandGitHub 3d8242fe5e Merge pull request #496 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.31.0 2024-11-14 12:16:25 -05:00
dependabot[bot]andGitHub 41a89239af Bump golang.org/x/net from 0.30.0 to 0.31.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/net/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 05:45:30 +00:00
Martin AngersandGitHub 53e7498d1b Merge pull request #491 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.30.0
Bump golang.org/x/net from 0.29.0 to 0.30.0
2024-10-08 20:20:17 -04:00
dependabot[bot]andGitHub ae85fefd98 Bump golang.org/x/net from 0.29.0 to 0.30.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/net/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 05:35:20 +00:00
Martin Angers 7d37930884 Update readme to prepare 1.10 version 2024-09-06 10:34:28 -04:00
Martin AngersandGitHub 9551ee516e Merge pull request #489 from PuerkitoBio/amikai-itereach-pr
Support EachIter based on Go's 1.23 iterator.
2024-09-06 10:28:29 -04:00
Martin Angers 291e2cf4e3 Merge branch 'master' into amikai-itereach-pr 2024-09-06 10:25:21 -04:00
Martin Angers 7019296f03 Update CI to target go1.23+ 2024-09-06 10:21:11 -04:00
Martin Angers eec8e87fed Revert "Re-orangize code for compitable"
This reverts commit 6802fc5555.
2024-09-06 10:14:32 -04:00
Martin Angers 957a864443 Prepare 1.9.3 release 2024-09-06 10:05:29 -04:00
Martin AngersandGitHub 34d91dbc67 Merge pull request #488 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.29.0
Bump golang.org/x/net from 0.28.0 to 0.29.0
2024-09-06 10:00:00 -04:00
dependabot[bot]andGitHub 0984c44bae Bump golang.org/x/net from 0.28.0 to 0.29.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/net/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-06 06:00:31 +00:00
Martin AngersandGitHub fbfb78c352 Merge pull request #487 from Kiyo510/fix_ci_test
Resolve WARNING in CI of github actions.
2024-08-12 18:08:05 -04:00
Kiyo510 e91ee4d790 Fixed checkout to repo before executing actions/setup-go@v5 step to avoid error that go.mod does not exist 2024-08-12 22:23:40 +09:00
Martin AngersandGitHub 90131ce208 Merge pull request #485 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.28.0
Bump golang.org/x/net from 0.27.0 to 0.28.0
2024-08-07 21:04:20 -04:00
dependabot[bot]andGitHub 3e5761b538 Bump golang.org/x/net from 0.27.0 to 0.28.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/net/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-07 05:37:47 +00:00
amikai 6802fc5555 Re-orangize code for compitable
- We don't need to use the iter.Seq2 type; instead, we can use func(yield func(int, *Selection) bool). In fact, the underlying type of iter.Seq2 is func(yield func(int, *Selection) bool), so we don't need to upgrade the go.mod version.
- For for-range testing in version 1.23, we can use build tags to compile only for versions above 1.23.
2024-07-24 15:02:18 +08:00
Martin AngersandGitHub 466380eec7 Update test.yml (another attempt to use the go1.23 rc version) 2024-07-23 10:08:48 -04:00
Martin AngersandGitHub 0f5ed94072 Update test.yml to use go1.23rc2 2024-07-23 10:04:34 -04:00
amikai 3f0ba08339 Support EachIter based on 1.23 iterator 2024-07-20 11:38:01 +08:00
amikai 629afd96a7 Bump the version to 1.23 2024-07-20 11:37:24 +08:00
Martin AngersandGitHub f1f5362a06 Merge pull request #481 from conneroisu/patch-1
Update README.md description for seltabl to include provided language server
2024-07-17 20:42:27 -04:00
conneroandGitHub dd1e02fa62 Update README.md 2024-07-17 17:58:32 -04:00
Martin AngersandGitHub f9e713566d Merge pull request #480 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.27.0
Bump golang.org/x/net from 0.26.0 to 0.27.0
2024-07-08 19:14:06 -04:00
dependabot[bot]andGitHub fd5dc59310 Bump golang.org/x/net from 0.26.0 to 0.27.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/net/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 05:45:20 +00:00
Martin AngersandGitHub b752fff338 Merge pull request #479 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.26.0
Bump golang.org/x/net from 0.24.0 to 0.26.0
2024-07-02 21:10:10 -04:00
dependabot[bot]andGitHub 8bd5b7d432 Bump golang.org/x/net from 0.24.0 to 0.26.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.24.0 to 0.26.0.
- [Commits](https://github.com/golang/net/compare/v0.24.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-02 21:04:32 +00:00
Martin AngersandGitHub bf8e18a449 Merge pull request #478 from deining/fix-warnings
Bump GitHub workflows to their latest versions
2024-07-02 17:04:10 -04:00
Andreas Deininger 400486e492 Bump GitHub workflows to their latest versions 2024-07-02 21:54:31 +02:00
Martin AngersandGitHub 1bc4a06d02 Merge pull request #477 from conneroisu/master
Update README.md
2024-06-09 15:56:11 -04:00
conneroandGitHub bc419c1e56 Update README.md 2024-06-09 12:58:55 -05:00
Martin Angers b076e25085 Prepare for release v1.9.2 2024-04-29 20:41:06 -04:00
Martin AngersandGitHub 22d8cfd154 Merge pull request #470 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.23.0
Bump golang.org/x/net from 0.21.0 to 0.23.0
2024-04-19 11:27:59 -04:00
dependabot[bot]andGitHub 56c8ec7392 Bump golang.org/x/net from 0.21.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 13:10:04 +00:00
Martin Angers 3ce74287b9 Prepare new minor version 2024-02-29 09:27:33 -05:00
Martin Angers 153f1ea15a Add notes on cascadia behavior that may differ from jQuery`s 2024-02-29 09:22:34 -05:00
Martin AngersandGitHub 2095230494 Merge pull request #469 from jwilsson/pre-allocate-slice-in-map
Pre-allocate slice in generic Map function
2024-02-29 09:10:15 -05:00
Jonathan Wilsson ce7330a252 Pre-allocate slice in generic Map function 2024-02-28 21:02:30 +01:00
Martin Angers 4cbe087f02 Update dependencies 2024-02-22 17:19:35 -05:00
Martin Angers a47543523f Prepare README for the new version 2024-02-22 17:17:43 -05:00
Martin Angers 86d245603e Update CI to run Go 1.21 and 1.22 (latest 2 versions at the moment) 2024-02-22 17:10:10 -05:00
Martin Angers 1fad3d426b Implement Selection.Map by calling the generic Map
Tests pass and verified that benchmarks are basically the same,
including memory allocation.
2024-02-22 17:08:26 -05:00
Martin AngersandGitHub de2d209e9e Merge pull request #467 from Fesaa/feature/generics
Add a generic form of Selection.Map
2024-02-22 17:03:48 -05:00
Amelia 65c0ed3569 Impl PuerkitoBio/goquery#466 2024-02-22 17:12:50 +01:00
Martin AngersandGitHub b6c7644395 Merge pull request #464 from peterjasc/patch-1
Add related projects to Readme
2024-01-14 09:45:59 -05:00
Martin AngersandGitHub 11947b0fdf Remove paren 2024-01-14 09:43:31 -05:00
peterjascandGitHub 0f8eca33fe Update README.md 2024-01-14 12:53:37 +01:00
Martin AngersandGitHub fee38068fd Merge pull request #460 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.17.0
Bump golang.org/x/net from 0.15.0 to 0.17.0
2023-10-12 11:16:35 -04:00
dependabot[bot]andGitHub 6216851ea7 Bump golang.org/x/net from 0.15.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.15.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.15.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-11 23:15:50 +00:00
Martin AngersandGitHub 2f635a81ad Merge pull request #459 from jcconnell/update_dependencies
Update golang.org/x/net -> v0.15.0
2023-10-05 08:44:23 -04:00
JC Connell 527d85980c Update golang.org/x/net -> v0.15.0 2023-10-02 19:07:01 -07:00
Martin AngersandGitHub fb60260fc2 Merge pull request #457 from PuerkitoBio/mna-update-readme
Update README.md
2023-09-06 21:47:07 -04:00
Martin AngersandGitHub 6c7e5111a3 Merge pull request #456 from PuerkitoBio/mna-update-go-versions
Update test.yml to test on latest 2 Go versions.
2023-09-06 21:44:32 -04:00
Martin AngersandGitHub 3ceadf7b6c Update README.md 2023-09-06 21:42:24 -04:00
Martin AngersandGitHub 361d1ad92e Update test.yml to test on latest 2 Go versions. 2023-09-06 21:39:24 -04:00
Martin AngersandGitHub 2dbc26aca4 Merge pull request #452 from rulu158/update_dependencies
Update Go dependencies
2023-08-17 15:28:51 -04:00
Raúl García 8b62ea92ad Update Go dependencies 2023-08-17 20:40:37 +02:00
Martin AngersandGitHub 03e170457f Merge pull request #450 from coderwithcat/master
fix typo in README.md
2023-07-10 12:56:25 -04:00
cui fliter 8442cfcc57 fix typo in README.md
Signed-off-by: cui fliter <imcusg@gmail.com>
2023-07-10 12:01:47 +08:00
Martin AngersandGitHub b5ecb79c0b Merge pull request #444 from jauderho/master
Update Go dependencies
2023-04-18 21:05:07 -04:00
Jauder Ho 1be7481ad4 Update Go dependencies
Signed-off-by: Jauder Ho <jauderho@users.noreply.github.com>
2023-04-18 20:20:03 +00:00
Martin Angers 39fb6d4dc4 Prepare for patch release 2023-02-18 10:29:26 -05:00
Martin AngersandGitHub 33955beabb Merge pull request #439 from PuerkitoBio/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.2.0 to 0.7.0
2023-02-18 10:24:07 -05:00
dependabot[bot]andGitHub 015f32a1b4 Bump golang.org/x/net from 0.2.0 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.2.0 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.2.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-18 03:21:47 +00:00
Martin AngersandGitHub 3b7929a0d7 Merge pull request #434 from PuerkitoBio/update-go
Update Go versions used in CI
2022-11-17 10:09:36 -05:00
Martin Angers 53d8b4dd6f Update Go versions used in CI 2022-11-17 10:06:24 -05:00
Martin Angers b103c0e59a Merge remote-tracking branch 'refs/remotes/origin/master' 2022-11-16 21:09:11 -05:00
Martin Angers 36e95634e3 Update goquery dependencies 2022-11-16 21:08:40 -05:00
Martin AngersandGitHub 156fa4f6ae Merge pull request #416 from davemolk/readme-add-goGetJS
add goGetJS to readme
2022-06-10 09:48:40 -04:00
davemolk c76525c424 add goGetJS to readme 2022-06-10 05:05:08 -06:00
Martin AngersandGitHub 3920ccc98a Merge pull request #415 from jakopako/add-goskyr-to-related-projects
README.md: Add goskyr to related projects.
2022-05-06 08:18:09 -04:00
jakopako eef406fe05 added goskyr to related projects 2022-05-06 13:37:08 +02:00
Martin AngersandGitHub ed0b238f66 Merge pull request #412 from mitar/patch-1
There is no InnerHtml.
2022-04-23 19:05:04 -04:00
MitarandGitHub 93ca487049 There is no InnerHtml. 2022-04-23 14:34:58 -07:00
Martin AngersandGitHub 8c90f82d4f Merge pull request #404 from jauderho/master
Updated go.mod dependencies per Snyk alert
2022-01-04 14:43:08 -05:00
Jauder Ho 5ffcf47d06 Updated go.mod dependencies per Snyk alert
Signed-off-by: Jauder Ho <jauderho@users.noreply.github.com>
2022-01-04 17:28:09 +00:00
Martin Angers 7a43c97117 Make documentation more consistent 2021-10-25 20:31:05 -04:00
Martin Angers a8c788f298 Add new version to changelog 2021-10-25 20:24:24 -04:00
Martin Angers faf9b4dc84 Remove travis ci, add github actions badge 2021-10-25 20:21:06 -04:00
Martin Angers a751e062fd Remove sourcehut build, use github actions 2021-10-25 20:18:35 -04:00
Martin Angers b6dc0bd0ea Fix some small linting issues 2021-10-25 20:16:05 -04:00
Martin AngersandGitHub 4110e175b5 Merge pull request #399 from anthonygedeon/issue/377-save-manipulated-html
Fixes #377, renders the html that can be manipulated with writer
2021-10-25 20:13:01 -04:00
Anthony Gedeon d35d4cec3e fix(doc): update docs for accurate representation of function 2021-10-25 18:16:30 -05:00
Anthony Gedeon 11e1233457 fix(test): remove Render testcase since OuterHtml handles Render internally 2021-10-25 18:16:09 -05:00
Anthony Gedeon 98eb88c22b Fixes PuerkitoBio#377, renders the html that can be manipulated with an writer as input 2021-10-24 21:08:06 -05:00
Martin AngersandGitHub 6a7f1c4a50 Merge pull request #394 from kinoute/master
upgrade Cascadia
2021-09-25 16:11:08 -04:00
Yann Defretin 7307e2457c upgrade Cascadia 2021-09-25 22:04:20 +02:00
Martin AngersandGitHub a7412da07a Merge pull request #392 from kinoute/master
use new Cascadia version
2021-09-22 17:38:44 -04:00
Yann Defretin c5eeb05f52 use new Cascadia version 2021-09-22 19:09:37 +02:00
Martin Angers 372d5bdaa0 Add changelog 2021-07-11 09:51:06 -04:00
Martin Angers 5dfda0e354 Merge branch 'jauderho-update-gomod' 2021-07-11 09:49:15 -04:00
Jauder Ho d6951d24bb Update go modules 2021-07-11 05:44:15 +00:00
Jauder HoandGitHub c09491e08b Update dependabot.yml 2021-07-11 05:37:44 +00:00
Jauder HoandGitHub 02e233c33a Create dependabot.yml 2021-07-11 05:36:11 +00:00
Martin Angers e11510b114 Update changelog for 1.7.0 2021-06-14 09:16:48 -04:00
Martin Angers a9cfd6779b Merge branch 'wip-i255-single-match' 2021-06-14 09:09:43 -04:00
Martin Angers b3953ef42a Finalize documentation 2021-06-14 09:06:47 -04:00
Martin Angers ee8a7e0a3a Test and benchmark the Single[Matcher] funcs 2021-06-13 15:02:43 -04:00
Martin Angers 0126a1fd88 Implement Single and SingleMatcher 2021-06-13 14:34:32 -04:00
Martin AngersandGitHub e1f2d60505 Merge pull request #381 from j0nimost/readme-fix
Fixed ExampleScrape README Sample
2021-06-04 14:51:04 -04:00
John Nyingi 1ba0ef7e9b Fixed ExampleScrape Sample 2021-06-04 21:37:05 +03:00
Martin Angers 795f67a092 Update doc badge 2021-05-19 11:06:14 -04:00
Martin Angers b67d192c91 Add funding file 2021-05-19 11:03:18 -04:00
Martin Angers bedb4e7a88 Resubmit build 2021-01-18 18:13:21 -05:00
Martin Angers a29eef7e71 Update go versions in travis tests 2021-01-18 17:11:31 -05:00
Martin Angers aa734f3932 add sourcehut build badge 2021-01-11 20:02:20 -05:00
Martin Angers 74649be271 fix go versions 2021-01-11 19:56:54 -05:00
Martin Angers 9d962837e1 try to fix build manifest 2021-01-11 19:24:59 -05:00
Martin Angers 7fc894a0ba add other go versions to build manifest 2021-01-11 19:17:56 -05:00
Martin Angers 171b719dd6 add fedora build manifest 2021-01-11 19:07:57 -05:00
Martin Angers f41c56001f Cut new release 2021-01-11 18:44:58 -05:00
Martin Angers 8a11cc402f Ignore non-element nodes when inserting html in a selection 2021-01-11 18:43:18 -05:00
Martin AngersandGitHub 70a02e53e3 Merge pull request #364 from santosh653/master
AddingPowerSupport_CI/Testing
2020-12-08 08:09:26 -05:00
santosh653andGitHub 3798c63c0d Update .travis.yml
Excluding unsupported versions for Power., ie go <=1.12.x
2020-12-07 23:31:38 -05:00
santosh653andGitHub 705a0066a5 Update .travis.yml
Adding power support & updating the go versions to >=1.13 as lower versions are not supported.,
2020-12-07 06:53:59 -05:00
Martin AngersandGitHub a48bafac4b Merge pull request #362 from vhodges/master
Update README.md, closes #361
2020-11-30 18:58:28 -05:00
Vince HodgesandGitHub af965a4d72 Update README.md 2020-11-30 15:54:55 -08:00
Martin Angers b3706f8903 update readme to prepare for new release 2020-10-08 09:16:43 -04:00
Martin AngersandGitHub 7ebd145bd7 Merge pull request #348 from thiemok/context
Send context to parseHtml. Closes #178 .
2020-10-08 09:10:18 -04:00
Martin Angers 4a687a672f add test cases to check html insertion order 2020-10-08 09:02:13 -04:00
thiemok 94c2530dd8 fix(manipulation): fix node insertion and caching issues
Node caching was still using the node type instead of the node name in a
few places. Updated those to also use the node name as the cache key.

ReplaceWithHtml and PrependHtml inserted parsed nodes in the wrong
order. Kept track of the correct parent to fix this.

Use correct parent flag for SetHtml
2020-10-07 20:04:45 +02:00
Martin Angers 5d7b0271f0 add tests to confirm erroneous behaviour 2020-10-07 10:54:01 -04:00
thiemok d89086ead1 fix(manipulation): address issues from code review of #235
* SetHml remains unchanged, see comment on the isParent param of
eachNodeHtml
* Removed unused functions
* Added documentation on eachNodeHtml
* Fixed node parsing cache keys and added test
* Refactored some tests for clarity
2020-09-28 15:37:51 +02:00
David Wilkinsandthiemok 99c8f1ecd5 Fixes #178 - Send context to parseHtml 2020-09-25 18:45:19 +02:00
Martin AngersandGitHub c942deb265 Merge pull request #347 from AbhigyanKumar/patch-1
Go syntax added
2020-09-24 09:53:53 -04:00
Abhigyan KumarandGitHub 0132bb8147 Go syntax
Go syntax colouring added.
2020-09-24 19:10:37 +05:30
Martin Angers 6f0f9d6b87 Merge branch 'eclipseo-fix_conversion_int_to_string' 2020-08-01 19:32:31 -04:00
Martin Angers cd12820764 use strconv.Itoa instead of fmt.Sprintf 2020-08-01 19:32:09 -04:00
Robert-André Mauchin 51a0e1a4bb Convert int to string using fmt.Sprintf
See https://github.com/golang/go/issues/32479

Fix #341.

Signed-off-by: Robert-André Mauchin <zebob.m@gmail.com>
2020-07-31 23:58:32 +02:00
Martin AngersandGitHub 89946c829f Merge pull request #330 from foolin/master
add [Pagser] to Related Projects
2020-04-19 11:21:48 -04:00
foolin 8b2a6e25d5 fix an to a 2020-04-19 23:18:31 +08:00
foolin 538dd4a913 add [Pagser] to Related Projects 2020-04-19 23:04:27 +08:00
Martin Angers 844d6bd46b doc: clarify how RemoveFiltered works 2020-02-24 18:34:32 -05:00
Martin Angers 1b16b92ddf doc: add 1.5.1 2020-02-04 18:10:22 -05:00
Martin AngersandGitHub 3d11372b35 Merge pull request #321 from akkumar/gomodupgrade
feat: upgrade gomod
2020-02-04 18:08:31 -05:00
Karthik K bd23f0c6e0 feat: upgrade gomod 2020-02-04 13:00:44 +05:30
Martin Angers 56ba1e6f08 add Geziyor to related projects 2019-12-09 15:00:45 -05:00
Martin Angers b17a2bbb6d remove go1.1 from travis, consistently fails 2019-10-15 18:09:08 -04:00
Martin Angers c70bac62f5 add Go versions to travis 2019-10-15 18:07:18 -04:00
Martin AngersandGitHub 8674076365 Merge pull request #310 from theophanous/colly-readme-patch
Updated colly project reference in README.md
2019-10-15 18:05:36 -04:00
Peter TheophanousandGitHub 6dcff85b2e Updated colly project reference in README.md 2019-10-14 21:58:31 +01:00
Martin AngersandGitHub 3dcf72e6c1 Merge pull request #280 from slotix/master
README.md: Add "Dataflow kit" to related projects.
2019-01-09 18:07:04 -05:00
Dmitry NarizhnykhandGitHub 22fd749627 README.md: Add "Dataflow kit" to related projects. 2019-01-09 22:44:10 +01:00
Martin AngersandGitHub 9b813b2309 Merge pull request #277 from tauraamui/tauraamui-update-readme
Update README.md to include BerryCMS in related
2019-01-08 07:15:36 -05:00
Adam Prakash LewisandGitHub 0255f16068 Update README.md to include BerryCMS in related 2019-01-08 09:36:23 +00:00
Martin Angers 2d2796f417 doc: 1.5 release with go modules 2018-11-15 15:42:34 -05:00
Martin AngersandGitHub 80f0084f4c Merge pull request #267 from Zaba505/master
Added Go Module support
2018-11-15 15:40:51 -05:00
Carson Derr 68fe358134 Add Go Module support 2018-11-14 19:30:47 -05:00
Martin AngersandGitHub 2af3d16e2b Merge pull request #261 from msoap/patch-1
Added Go 1.11 on travis CI
2018-10-14 13:58:06 -04:00
Sergey MudrikandGitHub 6c2cec4567 Added Go 1.11 on travis CI 2018-10-14 20:07:07 +03:00
Martin Angers 8311f594d7 readme: add ferret in projects using goquery 2018-10-02 20:21:05 -04:00
Martin Angers dc2ec5c7ca release 1.4.1 2018-06-07 11:06:10 -04:00
Martin AngersandGitHub 7757e27950 add examples for NewDocumentFromReader (#254) 2018-06-07 11:04:59 -04:00
Martin Angers ea1bc64a63 fix example indentation 2018-03-24 12:22:12 -04:00
Martin Angers dd77530fdb update examples to use the preferred NewDocumentFromReader approach 2018-03-24 12:12:09 -04:00
Martin AngersandGitHub a86ea07301 Merge pull request #247 from PuerkitoBio/wip-deprecate
Deprecate NewDocument and NewDocumentFromResponse. Closes #173 .
2018-03-24 12:02:17 -04:00
Martin Angers b4912d23e5 document the new release 2018-03-24 11:52:59 -04:00
Martin Angers 106fcbf95a deprecate NewDocument and NewDocumentFromResponse 2018-03-24 11:50:21 -04:00
Martin AngersandGitHub 61aa1975b1 Merge pull request #243 from gnulnx/master
Added gnulnx/goperf to Related Projects
2018-03-13 18:36:54 -04:00
John Furr 7a8f63580d Added gnulnx/goperf to Related Projects 2018-03-13 16:31:29 -04:00
Martin Angers 6285ceb1e9 last attempt to get go1.1 with travis 2018-03-10 15:43:14 -05:00
Martin Angers b55d492104 travis go1.1 gets 1.7 for some reason 2018-03-10 15:37:49 -05:00
Martin Angers ef34a70bad add 1.10 in travis config 2018-03-10 15:30:08 -05:00
Martin AngersandGitHub 5819122076 add Support section to readme (#241) 2018-03-10 15:26:54 -05:00
Martin AngersandGitHub 09540e5659 Merge pull request #234 from PuerkitoBio/wip-slice-end
Add ToEnd constant to use as special end index in Slice, closes #232.
2018-01-28 14:56:50 -05:00
Martin Angers 46f28d87ad document addition of ToEnd 2018-01-28 14:43:53 -05:00
Martin Angers 520f19d599 add ToEnd marker to Slice until the end of the Selection 2018-01-28 14:41:06 -05:00
Martin AngersandGitHub 2324bda66f Merge pull request #231 from Strum355/patch-1
Removed unnecessary conditional check
2018-01-18 09:26:56 -05:00
Noah Santschi-CooneyandGitHub 3855699cc0 Removed unnecessary if
IsMatcher returns false if len(s.Nodes) !> 0, so Is was double checking if len(s.Nodes) > 0
2018-01-17 13:49:09 +00:00
Martin Angers a41cf096e6 update changelog 2018-01-11 08:54:59 -05:00
Martin Angers 7d9aa5c1e6 misc/git: remove excessively verbose lint tools 2018-01-11 08:49:46 -05:00
Martin Angers c56812c109 fix godoc comment to comply with golint 2018-01-11 08:48:49 -05:00
Martin AngersandGitHub cab5e2c9d6 Merge pull request #229 from davidjwilkins/feature-226
Fixes #226 - Added the AddBack, AddBackFiltered, and AddBackMatcher methods.
2018-01-11 08:45:45 -05:00
David Wilkins 802bece6d0 Move deprecated notice to end of comment 2018-01-10 21:36:54 -08:00
David Wilkins 499bd47957 Fixes #226 - Added the AddBack, AddBackFiltered, and AddBackMatcher methods. Changed AndSelf to simply be an alias for AddBack, and added a deprecation notice to AddBack (as with https://api.jquery.com/andself/) 2018-01-09 20:34:18 -08:00
Martin AngersandGitHub bc4e06eb07 Merge pull request #221 from y-yagi/patch-1
Add go1.9 to travis
2017-12-06 07:16:06 -05:00
Yuji YaginumaandGitHub d8ab9abbf4 Add go1.9 to travis 2017-12-06 18:46:13 +09:00
Martin AngersandGitHub 1a71cc719d Merge pull request #215 from kataras/patch-1
README.md: Add colly - Scraping Framework to the Related Projects
2017-10-07 10:12:28 -04:00
Gerasimos (Makis) MaropoulosandGitHub 4b3a6d701e README.md: Add colly - Scraping Framework to the Related Projects 2017-10-07 07:57:40 +03:00
Martin AngersandGitHub 63e7f870c2 Merge pull request #211 from 38elements/fix-typo
Fix typo
2017-09-29 06:56:09 -04:00
38elementsandGitHub 109d3a6a56 Fix typo 2017-09-29 18:51:07 +09:00
Martin Angers ce645ea5e6 use svg travis badge, master branch 2017-08-16 09:38:31 -04:00
Martin Angers 8806ada2a4 update README with "related projects" section instead of just "downstream" 2017-06-23 12:21:02 -04:00
Martin AngersandGitHub 2dc93891ab Merge pull request #183 from andrewstuart/master
Add goquery marshaling library as a downstream project in Readme
2017-05-20 15:45:20 -04:00
Andrew Stuart 4a7586a83f Update readme with some downstream projects 2017-05-15 16:49:20 -07:00
Martin Angers ed7d758e9a add 1.8, test on latest patches 2017-03-24 09:54:48 -04:00
Martin AngersandGitHub c641b87978 Merge pull request #162 from rohanpai/master
Added Sourcegraph Badge
2017-03-08 14:48:52 -05:00
Rohan PaiandGitHub 28076b7c97 Added Sourcegraph Badge 2017-03-08 11:27:24 -08:00
Martin Angers e1271ee34c Merge branch 'glebtv-setHtml' 2017-02-12 11:36:42 -05:00
Martin Angers f5c5de8530 update readme for new release 2017-02-12 11:36:10 -05:00
Martin Angers 46c32cef0e unexport SetNodes, add SetHtml tests 2017-02-12 11:34:10 -05:00
Gleb Tv 755fd592bb implement SetHtml and SetText, fixes #123 2017-02-08 16:27:36 +03:00
Martin AngersandGitHub 3cb3b86568 Update README.md 2016-12-29 22:41:54 -05:00
Martin AngersandGitHub 2ee2e49ac5 Merge pull request #142 from radovskyb/master
Optimizes the *Selection.Text method and reduces allocations.
2016-12-29 22:37:29 -05:00
Benjamin Radovsky c0b805c7dc greatly optimize the *Selection.Text method and highly reduce it's amount of allocations per op. 2016-12-27 17:37:31 +11:00
Martin Angers 152b1a2c8f remove wiki section on big docs, cascadia is now almost as fast as workarounds 2016-08-31 12:22:11 -04:00
Martin Angers 9043fe2599 add tip for large documents 2016-08-30 20:10:13 -04:00
Martin Angers 56a198aba8 add go1.7 to travis 2016-08-28 21:21:01 -04:00
Martin Angers 5dccaa115b optimize winnowNodes and ClosestNodes in the same way 2016-08-28 11:55:36 -04:00
Martin Angers 26f4f54c91 simplify string search expressions, remove unused code 2016-08-28 11:09:29 -04:00
Martin Angers e89fe653fd update README for v1.0.1 2016-08-28 11:06:06 -04:00
Martin Angers ab2bb46123 update pre-commit hook with other static checks 2016-08-28 11:04:26 -04:00
Martin Angers b91267330e add tests and benchmark for appendWithoutDuplicates optimization 2016-08-28 11:03:18 -04:00
Martin Angers d85ad7e2ed remove var declaration 2016-08-27 22:44:10 -04:00
Martin Angers 6841d28c6f comment and explain the appendWithoutDuplicates logic 2016-08-27 22:29:53 -04:00
Martin Angers 7bd630023a reuse the deduplication set map in mapNodes 2016-08-27 22:25:02 -04:00
Martin Angers c7533ba696 maintain a set of existing target nodes to deduplicate
The old implementation used nested loops to check if one of the
new nodes was already in the existing nodes. This commit
maintains a map[*html.Node]bool so that a single loop over
the new elements is required. This is slower for small
slices though, and allocates more. The next step is for
appendWithoutDuplicates to accept a map as argument so
that the set is not recreated on each call from mapNodes.
2016-08-27 18:55:21 -04:00
Martin Angers ad8b3d8222 remove logging from benchmarks, validate expected result instead 2016-08-27 18:37:55 -04:00
Martin Angers 7f4291566c tag v1.0.0, closes #124 2016-07-27 21:43:13 -04:00
Martin Angers f0d75731e0 fix example, closes #120 2016-06-29 08:59:28 -04:00
Martin AngersandGitHub 77cbaef46a Merge pull request #115 from PuerkitoBio/i114
Treat invalid selector strings as a Matcher that doesn't match anything
2016-06-15 09:48:05 -04:00
Martin Angers 2441483128 update doc 2016-06-15 09:40:19 -04:00
Martin Angers 2ab590df05 use a failing matcher if selector fails to compile, adjust tests 2016-06-15 09:27:17 -04:00
Martin Angers 9cc531274b implement tests for selection methods with invalid selector strings 2016-06-15 08:42:27 -04:00
Martin Angers 2cd8b4e49a test all go versions since 1.1 2016-06-15 07:50:27 -04:00
Martin Angers aeecb06b1e doc: better explain the arguments to the function passed to Each and Map, closes #107 2016-05-26 08:12:20 -04:00
Martin Angers 2e29ea41f0 add a test for issue #103 2016-04-08 07:41:18 -04:00
Martin Angers 5cc721859e edit example to follow Go conventions 2016-04-08 07:39:02 -04:00
Martin Angers 4126223a86 exclude testdata/ files from github language stats 2016-02-16 20:35:17 -05:00
Martin Angers 81e6517758 doc: fix list for godoc 2016-02-02 12:40:27 -05:00
Martin Angers bb16855614 Merge pull request #102 from PuerkitoBio/helper-funcs
add NodeName and OuterHtml utility functions, closes #96 and #101 .
2016-02-02 12:37:00 -05:00
45 changed files with 2504 additions and 403 deletions
+1
View File
@@ -0,0 +1 @@
testdata/* linguist-vendored
+2
View File
@@ -0,0 +1,2 @@
github: [mna]
custom: ["https://www.buymeacoffee.com/mna"]
+12
View File
@@ -0,0 +1,12 @@
version: 2
updates:
# Maintain dependencies for Go
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
# Enable version updates for GitHub action workflows
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
+25
View File
@@ -0,0 +1,25 @@
name: test
on: [push, pull_request]
env:
GOPROXY: https://proxy.golang.org,direct
jobs:
test:
strategy:
matrix:
go-version: [1.26.x, 1.27.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install Go
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go-version }}
- name: Test
run: go test ./... -v -cover
+10 -1
View File
@@ -1,7 +1,16 @@
# editor temporary files
*.sublime-*
.DS_Store
*.swp
#*.*#
tags
goquery.test
# direnv config
.env*
# test binaries
*.test
# coverage and profilte outputs
*.out
-7
View File
@@ -1,7 +0,0 @@
language: go
go:
- 1.1
- 1.2
- 1.3
- tip
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2012-2014, Martin Angers & Contributors
Copyright (c) 2012-2021, 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:
+127 -22
View File
@@ -1,8 +1,8 @@
# 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)
[![Build Status](https://github.com/PuerkitoBio/goquery/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/PuerkitoBio/goquery/actions)
[![Go Reference](https://pkg.go.dev/badge/github.com/PuerkitoBio/goquery.svg)](https://pkg.go.dev/github.com/PuerkitoBio/goquery)
[![Sourcegraph Badge](https://sourcegraph.com/github.com/PuerkitoBio/goquery/-/badge.svg)](https://sourcegraph.com/github.com/PuerkitoBio/goquery?badge)
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.
@@ -10,14 +10,32 @@ Also, because the net/html parser requires UTF-8 encoding, so does goquery: it i
Syntax-wise, it is as close as possible to jQuery, with the same function names when possible, and that warm and fuzzy chainable interface. jQuery being the ultra-popular library that it is, I felt that writing a similar HTML-manipulating library was better to follow its API than to start anew (in the same spirit as Go's `fmt` package), even though some of its methods are less than intuitive (looking at you, [index()][index]...).
## Table of Contents
* [Installation](#installation)
* [Changelog](#changelog)
* [API](#api)
* [Examples](#examples)
* [Related Projects](#related-projects)
* [Support](#support)
* [License](#license)
## Installation
Please note that because of the net/html dependency, goquery requires Go1.1+.
Required Go version:
* Starting with version `v1.12.0` of goquery, Go 1.25+ is required due to its dependencies.
* Starting with version `v1.11.0` of goquery, Go 1.24+ is required due to its dependencies.
* Starting with version `v1.10.0` of goquery, Go 1.23+ is required due to the use of function-based iterators.
* For `v1.9.0` of goquery, Go 1.18+ is required due to the use of generics.
* For previous goquery versions, a Go version of 1.1+ was required because of the `net/html` dependency.
Ongoing goquery development is tested on the latest 2 versions of Go.
$ go get github.com/PuerkitoBio/goquery
(optional) To run unit tests:
$ cd $GOPATH/src/github.com/PuerkitoBio/goquery
$ go test
@@ -30,7 +48,35 @@ 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-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`).
* **2026-08-27 (v1.13.0)** : Performance improvements (thanks [@jvoisin][jvs]), add top-level `Text` function with options similar to BeautifulSoup's `get_text` (thanks [@ChrisJr404][chrisjr]), update `go.mod` dependencies, add go1.27 to the test matrix.
* **2026-03-15 (v1.12.0)** : Update `go.mod` dependencies, add go1.26 to the test matrix, **goquery now requires Go version 1.25+**.
* **2025-11-16 (v1.11.0)** : Update `go.mod` dependencies, add go1.25 to the test matrix, **goquery now requires Go version 1.24+**.
* **2025-04-11 (v1.10.3)** : Update `go.mod` dependencies, small optimization (thanks [@myxzlpltk](https://github.com/myxzlpltk)).
* **2025-02-13 (v1.10.2)** : Update `go.mod` dependencies, add go1.24 to the test matrix.
* **2024-12-26 (v1.10.1)** : Update `go.mod` dependencies.
* **2024-09-06 (v1.10.0)** : Add `EachIter` which provides an iterator that can be used in `for..range` loops on the `*Selection` object. **goquery now requires Go version 1.23+** (thanks [@amikai](https://github.com/amikai)).
* **2024-09-06 (v1.9.3)** : Update `go.mod` dependencies.
* **2024-04-29 (v1.9.2)** : Update `go.mod` dependencies.
* **2024-02-29 (v1.9.1)** : Improve allocation and performance of the `Map` function and `Selection.Map` method, better document the cascadia differences (thanks [@jwilsson](https://github.com/jwilsson)).
* **2024-02-22 (v1.9.0)** : Add a generic `Map` function, **goquery now requires Go version 1.18+** (thanks [@Fesaa](https://github.com/Fesaa)).
* **2023-02-18 (v1.8.1)** : Update `go.mod` dependencies, update CI workflow.
* **2021-10-25 (v1.8.0)** : Add `Render` function to render a `Selection` to an `io.Writer` (thanks [@anthonygedeon](https://github.com/anthonygedeon)).
* **2021-07-11 (v1.7.1)** : Update go.mod dependencies and add dependabot config (thanks [@jauderho](https://github.com/jauderho)).
* **2021-06-14 (v1.7.0)** : Add `Single` and `SingleMatcher` functions to optimize first-match selection (thanks [@gdollardollar](https://github.com/gdollardollar)).
* **2021-01-11 (v1.6.1)** : Fix panic when calling `{Prepend,Append,Set}Html` on a `Selection` that contains non-Element nodes.
* **2020-10-08 (v1.6.0)** : Parse html in context of the container node for all functions that deal with html strings (`AfterHtml`, `AppendHtml`, etc.). Thanks to [@thiemok][thiemok] and [@davidjwilkins][djw] for their work on this.
* **2020-02-04 (v1.5.1)** : Update module dependencies.
* **2018-11-15 (v1.5.0)** : Go module support (thanks @Zaba505).
* **2018-06-07 (v1.4.1)** : Add `NewDocumentFromReader` examples.
* **2018-03-24 (v1.4.0)** : Deprecate `NewDocument(url)` and `NewDocumentFromResponse(response)`.
* **2018-01-28 (v1.3.0)** : Add `ToEnd` constant to `Slice` until the end of the selection (thanks to @davidjwilkins for raising the issue).
* **2018-01-11 (v1.2.0)** : Add `AddBack*` and deprecate `AndSelf` (thanks to @davidjwilkins).
* **2017-02-12 (v1.1.0)** : Add `SetHtml` and `SetText` (thanks to @glebtv).
* **2016-12-29 (v1.0.2)** : Optimize allocations for `Selection.Text` (thanks to @radovskyb).
* **2016-08-28 (v1.0.1)** : Optimize performance for large documents.
* **2016-07-27 (v1.0.0)** : Tag version 1.0.0.
* **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 [doc][] 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].
* **2014-11-28** : Add more manipulation functions - ReplaceWith*, Wrap* and Unwrap - thanks again to [Andrew Stone][thatguystone].
@@ -59,15 +105,20 @@ jQuery often has many variants for the same function (no argument, a selector st
Utility functions that are not in jQuery but are useful in Go are implemented as functions (that take a `*Selection` as parameter), to avoid a potential naming clash on the `*Selection`'s methods (reserved for jQuery-equivalent behaviour).
The complete [godoc reference documentation can be found here][doc].
The complete [package 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. Also, the selectors work more like the DOM's `querySelectorAll`, than jQuery's matchers - they have no concept of contextual matching (for some concrete examples of what that means, see [this ticket](https://github.com/andybalholm/cascadia/issues/61)). In practice, it doesn't matter very often but it's something worth mentioning. 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
See some tips and tricks in the [wiki][].
Taken (and adapted as if executed from outside the goquery package) from example_test.go:
Adapted from example_test.go:
```Go
package main
@@ -75,21 +126,34 @@ package main
import (
"fmt"
"log"
"net/http"
"github.com/PuerkitoBio/goquery"
)
func ExampleScrape() {
doc, err := goquery.NewDocument("http://metalsucks.net")
// Request the HTML page.
res, err := http.Get("http://metalsucks.net")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
}
// Load the HTML document
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
log.Fatal(err)
}
doc.Find(".reviews-wrap article .review-rhs").Each(func(i int, s *goquery.Selection) {
band := s.Find("h3").Text()
title := s.Find("i").Text()
fmt.Printf("Review %d: %s - %s\n", i, band, title)
})
// Find the review items
doc.Find(".left-content article .post-title").Each(func(i int, s *goquery.Selection) {
// For each item found, get the title
title := s.Find("a").Text()
fmt.Printf("Review %d: %s\n", i, title)
})
}
func main() {
@@ -97,20 +161,61 @@ func main() {
}
```
## Related Projects
- [Goq][goq], an HTML deserialization and scraping library based on goquery and struct tags.
- [andybalholm/cascadia][cascadia], the CSS selector library used by goquery.
- [suntong/cascadia][cascadiacli], a command-line interface to the cascadia CSS selector library, useful to test selectors.
- [gocolly/colly](https://github.com/gocolly/colly), a lightning fast and elegant Scraping Framework
- [gnulnx/goperf](https://github.com/gnulnx/goperf), a website performance test tool that also fetches static assets.
- [MontFerret/ferret](https://github.com/MontFerret/ferret), declarative web scraping.
- [tacusci/berrycms](https://github.com/tacusci/berrycms), a modern simple to use CMS with easy to write plugins
- [Dataflow kit](https://github.com/slotix/dataflowkit), Web Scraping framework for Gophers.
- [Geziyor](https://github.com/geziyor/geziyor), a fast web crawling & scraping framework for Go. Supports JS rendering.
- [Pagser](https://github.com/foolin/pagser), a simple, easy, extensible, configurable HTML parser to struct based on goquery and struct tags.
- [stitcherd](https://github.com/vhodges/stitcherd), A server for doing server side includes using css selectors and DOM updates.
- [goskyr](https://github.com/jakopako/goskyr), an easily configurable command-line scraper written in Go.
- [goGetJS](https://github.com/davemolk/goGetJS), a tool for extracting, searching, and saving JavaScript files (with optional headless browser).
- [fitter](https://github.com/PxyUp/fitter), a tool for selecting values from JSON, XML, HTML and XPath formatted pages.
- [seltabl](github.com/conneroisu/seltabl), an orm-like package and supporting language server for extracting values from HTML
## Support
There are a number of ways you can support the project:
* Use it, star it, build something with it, spread the word!
- If you do build something open-source or otherwise publicly-visible, let me know so I can add it to the [Related Projects](#related-projects) section!
* Raise issues to improve the project (note: doc typos and clarifications are issues too!)
- Please search existing issues before opening a new one - it may have already been addressed.
* Pull requests: please discuss new code in an issue first, unless the fix is really trivial.
- Make sure new code is tested.
- Be mindful of existing code - PRs that break existing code have a high probability of being declined, unless it fixes a serious issue.
* Sponsor the developer
- See the Github Sponsor button at the top of the repo on github
- or via BuyMeACoffee.com, below
<a href="https://www.buymeacoffee.com/mna" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
## License
The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia's license is [here][caslic].
[jquery]: http://jquery.com/
[go]: http://golang.org/
[jquery]: https://jquery.com/
[go]: https://go.dev/
[cascadia]: https://github.com/andybalholm/cascadia
[bsd]: http://opensource.org/licenses/BSD-3-Clause
[golic]: http://golang.org/LICENSE
[cascadiacli]: https://github.com/suntong/cascadia
[bsd]: https://opensource.org/licenses/BSD-3-Clause
[golic]: https://go.dev/LICENSE
[caslic]: https://github.com/andybalholm/cascadia/blob/master/LICENSE
[doc]: http://godoc.org/github.com/PuerkitoBio/goquery
[index]: http://api.jquery.com/index/
[doc]: https://pkg.go.dev/github.com/PuerkitoBio/goquery
[index]: https://api.jquery.com/index/
[gonet]: https://github.com/golang/net/
[html]: http://godoc.org/golang.org/x/net/html
[html]: https://pkg.go.dev/golang.org/x/net/html
[wiki]: https://github.com/PuerkitoBio/goquery/wiki/Tips-and-tricks
[thatguystone]: https://github.com/thatguystone
[piotr]: https://github.com/piotrkowalczuk
[goq]: https://github.com/andrewstuart/goq
[thiemok]: https://github.com/thiemok
[djw]: https://github.com/davidjwilkins
[jvs]: https://github.com/jvoisin
[chrisjr]: https://github.com/ChrisJr404
+30 -3
View File
@@ -4,6 +4,16 @@ import (
"golang.org/x/net/html"
)
const (
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
// ToEnd is a special index value that can be used as end index in a call
// to Slice so that all elements are selected until the end of the Selection.
// It is equivalent to passing (*Selection).Length().
ToEnd = maxInt
)
// First reduces the set of matched elements to the first in the set.
// It returns a new Selection object, and an empty Selection object if the
// the selection is empty.
@@ -35,12 +45,23 @@ func (s *Selection) Eq(index int) *Selection {
}
// Slice reduces the set of matched elements to a subset specified by a range
// of indices.
// of indices. The start index is 0-based and indicates the index of the first
// element to select. The end index is 0-based and indicates the index at which
// the elements stop being selected (the end index is not selected).
//
// The indices may be negative, in which case they represent an offset from the
// end of the selection.
//
// The special value ToEnd may be specified as end index, in which case all elements
// until the end are selected. This works both for a positive and negative start
// index.
func (s *Selection) Slice(start, end int) *Selection {
if start < 0 {
start += len(s.Nodes)
}
if end < 0 {
if end == ToEnd {
end = len(s.Nodes)
} else if end < 0 {
end += len(s.Nodes)
}
return pushStack(s, s.Nodes[start:end])
@@ -60,7 +81,13 @@ func (s *Selection) Get(index int) *html.Node {
// relative to its sibling elements.
func (s *Selection) Index() int {
if len(s.Nodes) > 0 {
return newSingleSelection(s.Nodes[0], s.document).PrevAll().Length()
i := 0
for n := s.Nodes[0].PrevSibling; n != nil; n = n.PrevSibling {
if n.Type == html.ElementNode {
i++
}
}
return i
}
return -1
}
+54
View File
@@ -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)
@@ -83,6 +98,32 @@ func TestSlice(t *testing.T) {
sel := Doc().Find(".pvk-content").Slice(0, 2)
assertLength(t, sel.Nodes, 2)
assertSelectionIs(t, sel, "#pc1", "#pc2")
}
func TestSliceToEnd(t *testing.T) {
sel := Doc().Find(".pvk-content").Slice(1, ToEnd)
assertLength(t, sel.Nodes, 2)
assertSelectionIs(t, sel.Eq(0), "#pc2")
if _, ok := sel.Eq(1).Attr("id"); ok {
t.Error("Want no attribute ID, got one")
}
}
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 TestSliceInvalidToEnd(t *testing.T) {
defer assertPanic(t)
Doc().Find("").Slice(2, ToEnd)
}
func TestSliceOutOfBounds(t *testing.T) {
@@ -110,6 +151,12 @@ func TestNegativeSliceBoth(t *testing.T) {
assertSelectionIs(t, sel.Eq(1), "#cf3")
}
func TestNegativeSliceToEnd(t *testing.T) {
sel := Doc().Find(".container-fluid").Slice(-3, ToEnd)
assertLength(t, sel.Nodes, 3)
assertSelectionIs(t, sel, "#cf2", "#cf3", "#cf4")
}
func TestNegativeSliceOutOfBounds(t *testing.T) {
defer assertPanic(t)
Doc().Find(".container-fluid").Slice(-12, -7)
@@ -157,6 +204,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 {
+85
View File
@@ -0,0 +1,85 @@
BenchmarkFirst-4 30000000 50.7 ns/op 48 B/op 1 allocs/op
BenchmarkLast-4 30000000 50.9 ns/op 48 B/op 1 allocs/op
BenchmarkEq-4 30000000 55.7 ns/op 48 B/op 1 allocs/op
BenchmarkSlice-4 500000000 3.45 ns/op 0 B/op 0 allocs/op
BenchmarkGet-4 2000000000 1.68 ns/op 0 B/op 0 allocs/op
BenchmarkIndex-4 3000000 541 ns/op 248 B/op 10 allocs/op
BenchmarkIndexSelector-4 200000 10749 ns/op 2464 B/op 17 allocs/op
BenchmarkIndexOfNode-4 200000000 6.47 ns/op 0 B/op 0 allocs/op
BenchmarkIndexOfSelection-4 200000000 7.27 ns/op 0 B/op 0 allocs/op
BenchmarkMetalReviewExample-4 10000 138426 ns/op 12240 B/op 319 allocs/op
BenchmarkAdd-4 200000 10192 ns/op 208 B/op 9 allocs/op
BenchmarkAddSelection-4 10000000 158 ns/op 48 B/op 1 allocs/op
BenchmarkAddNodes-4 10000000 156 ns/op 48 B/op 1 allocs/op
BenchmarkAndSelf-4 1000000 1588 ns/op 1008 B/op 5 allocs/op
BenchmarkFilter-4 100000 20427 ns/op 360 B/op 8 allocs/op
BenchmarkNot-4 100000 23508 ns/op 136 B/op 5 allocs/op
BenchmarkFilterFunction-4 50000 34178 ns/op 22976 B/op 755 allocs/op
BenchmarkNotFunction-4 50000 38173 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterNodes-4 50000 34001 ns/op 20960 B/op 749 allocs/op
BenchmarkNotNodes-4 30000 40344 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterSelection-4 50000 33308 ns/op 20960 B/op 749 allocs/op
BenchmarkNotSelection-4 30000 40748 ns/op 29120 B/op 757 allocs/op
BenchmarkHas-4 5000 263346 ns/op 1816 B/op 48 allocs/op
BenchmarkHasNodes-4 10000 160840 ns/op 21184 B/op 752 allocs/op
BenchmarkHasSelection-4 10000 165410 ns/op 21184 B/op 752 allocs/op
BenchmarkEnd-4 2000000000 1.01 ns/op 0 B/op 0 allocs/op
BenchmarkEach-4 300000 4664 ns/op 3304 B/op 118 allocs/op
BenchmarkMap-4 200000 8286 ns/op 5572 B/op 184 allocs/op
BenchmarkEachWithBreak-4 2000000 806 ns/op 560 B/op 20 allocs/op
BenchmarkAttr-4 100000000 21.6 ns/op 0 B/op 0 allocs/op
BenchmarkText-4 200000 8909 ns/op 7536 B/op 110 allocs/op
BenchmarkLength-4 2000000000 0.34 ns/op 0 B/op 0 allocs/op
BenchmarkHtml-4 3000000 422 ns/op 120 B/op 2 allocs/op
BenchmarkIs-4 100000 22615 ns/op 88 B/op 4 allocs/op
BenchmarkIsPositional-4 50000 26655 ns/op 1112 B/op 10 allocs/op
BenchmarkIsFunction-4 1000000 1208 ns/op 784 B/op 28 allocs/op
BenchmarkIsSelection-4 50000 33497 ns/op 20960 B/op 749 allocs/op
BenchmarkIsNodes-4 50000 33572 ns/op 20960 B/op 749 allocs/op
BenchmarkHasClass-4 10000 232802 ns/op 14944 B/op 976 allocs/op
BenchmarkContains-4 200000000 7.33 ns/op 0 B/op 0 allocs/op
BenchmarkFind-4 200000 10715 ns/op 2464 B/op 17 allocs/op
BenchmarkFindWithinSelection-4 50000 35878 ns/op 2176 B/op 78 allocs/op
BenchmarkFindSelection-4 10000 194356 ns/op 2672 B/op 82 allocs/op
BenchmarkFindNodes-4 10000 195510 ns/op 2672 B/op 82 allocs/op
BenchmarkContents-4 1000000 2252 ns/op 864 B/op 34 allocs/op
BenchmarkContentsFiltered-4 500000 3015 ns/op 1016 B/op 39 allocs/op
BenchmarkChildren-4 5000000 364 ns/op 152 B/op 7 allocs/op
BenchmarkChildrenFiltered-4 1000000 2212 ns/op 352 B/op 15 allocs/op
BenchmarkParent-4 50000 24643 ns/op 4048 B/op 381 allocs/op
BenchmarkParentFiltered-4 50000 25967 ns/op 4248 B/op 388 allocs/op
BenchmarkParents-4 30000 50000 ns/op 27776 B/op 830 allocs/op
BenchmarkParentsFiltered-4 30000 53107 ns/op 28360 B/op 838 allocs/op
BenchmarkParentsUntil-4 100000 22423 ns/op 10352 B/op 353 allocs/op
BenchmarkParentsUntilSelection-4 20000 86925 ns/op 51144 B/op 1516 allocs/op
BenchmarkParentsUntilNodes-4 20000 87597 ns/op 51144 B/op 1516 allocs/op
BenchmarkParentsFilteredUntil-4 300000 5568 ns/op 2232 B/op 86 allocs/op
BenchmarkParentsFilteredUntilSelection-4 200000 10966 ns/op 5440 B/op 190 allocs/op
BenchmarkParentsFilteredUntilNodes-4 200000 10919 ns/op 5440 B/op 190 allocs/op
BenchmarkSiblings-4 30000 46018 ns/op 15400 B/op 204 allocs/op
BenchmarkSiblingsFiltered-4 30000 50566 ns/op 16496 B/op 213 allocs/op
BenchmarkNext-4 200000 7921 ns/op 3216 B/op 112 allocs/op
BenchmarkNextFiltered-4 200000 8804 ns/op 3416 B/op 118 allocs/op
BenchmarkNextAll-4 50000 31098 ns/op 9912 B/op 138 allocs/op
BenchmarkNextAllFiltered-4 50000 34677 ns/op 11008 B/op 147 allocs/op
BenchmarkPrev-4 200000 7920 ns/op 3216 B/op 112 allocs/op
BenchmarkPrevFiltered-4 200000 8913 ns/op 3416 B/op 118 allocs/op
BenchmarkPrevAll-4 200000 10845 ns/op 4376 B/op 113 allocs/op
BenchmarkPrevAllFiltered-4 100000 12030 ns/op 4576 B/op 119 allocs/op
BenchmarkNextUntil-4 100000 19193 ns/op 5760 B/op 260 allocs/op
BenchmarkNextUntilSelection-4 50000 34829 ns/op 18480 B/op 542 allocs/op
BenchmarkNextUntilNodes-4 100000 14459 ns/op 7944 B/op 248 allocs/op
BenchmarkPrevUntil-4 20000 66296 ns/op 12856 B/op 448 allocs/op
BenchmarkPrevUntilSelection-4 30000 45037 ns/op 23432 B/op 689 allocs/op
BenchmarkPrevUntilNodes-4 200000 11525 ns/op 6152 B/op 203 allocs/op
BenchmarkNextFilteredUntil-4 100000 12940 ns/op 4512 B/op 173 allocs/op
BenchmarkNextFilteredUntilSelection-4 50000 38924 ns/op 19160 B/op 567 allocs/op
BenchmarkNextFilteredUntilNodes-4 50000 38528 ns/op 19160 B/op 567 allocs/op
BenchmarkPrevFilteredUntil-4 100000 12980 ns/op 4664 B/op 175 allocs/op
BenchmarkPrevFilteredUntilSelection-4 50000 39671 ns/op 19936 B/op 587 allocs/op
BenchmarkPrevFilteredUntilNodes-4 50000 39484 ns/op 19936 B/op 587 allocs/op
BenchmarkClosest-4 500000 3310 ns/op 160 B/op 8 allocs/op
BenchmarkClosestSelection-4 5000000 361 ns/op 96 B/op 6 allocs/op
BenchmarkClosestNodes-4 5000000 359 ns/op 96 B/op 6 allocs/op
PASS
ok github.com/PuerkitoBio/goquery 163.718s
+85
View File
@@ -0,0 +1,85 @@
BenchmarkFirst-4 30000000 50.9 ns/op 48 B/op 1 allocs/op
BenchmarkLast-4 30000000 50.0 ns/op 48 B/op 1 allocs/op
BenchmarkEq-4 30000000 50.5 ns/op 48 B/op 1 allocs/op
BenchmarkSlice-4 500000000 3.53 ns/op 0 B/op 0 allocs/op
BenchmarkGet-4 2000000000 1.66 ns/op 0 B/op 0 allocs/op
BenchmarkIndex-4 2000000 832 ns/op 248 B/op 10 allocs/op
BenchmarkIndexSelector-4 100000 16073 ns/op 3839 B/op 21 allocs/op
BenchmarkIndexOfNode-4 200000000 6.38 ns/op 0 B/op 0 allocs/op
BenchmarkIndexOfSelection-4 200000000 7.14 ns/op 0 B/op 0 allocs/op
BenchmarkMetalReviewExample-4 10000 140737 ns/op 12418 B/op 320 allocs/op
BenchmarkAdd-4 100000 13162 ns/op 974 B/op 10 allocs/op
BenchmarkAddSelection-4 500000 3160 ns/op 814 B/op 2 allocs/op
BenchmarkAddNodes-4 500000 3159 ns/op 814 B/op 2 allocs/op
BenchmarkAndSelf-4 200000 7423 ns/op 2404 B/op 9 allocs/op
BenchmarkFilter-4 100000 19671 ns/op 360 B/op 8 allocs/op
BenchmarkNot-4 100000 22577 ns/op 136 B/op 5 allocs/op
BenchmarkFilterFunction-4 50000 33960 ns/op 22976 B/op 755 allocs/op
BenchmarkNotFunction-4 50000 37909 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterNodes-4 50000 34196 ns/op 20960 B/op 749 allocs/op
BenchmarkNotNodes-4 30000 40446 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterSelection-4 50000 33091 ns/op 20960 B/op 749 allocs/op
BenchmarkNotSelection-4 30000 40609 ns/op 29120 B/op 757 allocs/op
BenchmarkHas-4 5000 262936 ns/op 2371 B/op 50 allocs/op
BenchmarkHasNodes-4 10000 148631 ns/op 21184 B/op 752 allocs/op
BenchmarkHasSelection-4 10000 153117 ns/op 21184 B/op 752 allocs/op
BenchmarkEnd-4 2000000000 1.02 ns/op 0 B/op 0 allocs/op
BenchmarkEach-4 300000 4653 ns/op 3304 B/op 118 allocs/op
BenchmarkMap-4 200000 8257 ns/op 5572 B/op 184 allocs/op
BenchmarkEachWithBreak-4 2000000 806 ns/op 560 B/op 20 allocs/op
BenchmarkAttr-4 100000000 22.0 ns/op 0 B/op 0 allocs/op
BenchmarkText-4 200000 8913 ns/op 7536 B/op 110 allocs/op
BenchmarkLength-4 2000000000 0.35 ns/op 0 B/op 0 allocs/op
BenchmarkHtml-4 5000000 398 ns/op 120 B/op 2 allocs/op
BenchmarkIs-4 100000 22392 ns/op 88 B/op 4 allocs/op
BenchmarkIsPositional-4 50000 26259 ns/op 1112 B/op 10 allocs/op
BenchmarkIsFunction-4 1000000 1212 ns/op 784 B/op 28 allocs/op
BenchmarkIsSelection-4 50000 33222 ns/op 20960 B/op 749 allocs/op
BenchmarkIsNodes-4 50000 33408 ns/op 20960 B/op 749 allocs/op
BenchmarkHasClass-4 10000 233208 ns/op 14944 B/op 976 allocs/op
BenchmarkContains-4 200000000 7.57 ns/op 0 B/op 0 allocs/op
BenchmarkFind-4 100000 16121 ns/op 3839 B/op 21 allocs/op
BenchmarkFindWithinSelection-4 20000 68019 ns/op 11521 B/op 97 allocs/op
BenchmarkFindSelection-4 5000 387582 ns/op 59787 B/op 176 allocs/op
BenchmarkFindNodes-4 5000 389246 ns/op 59797 B/op 176 allocs/op
BenchmarkContents-4 200000 11475 ns/op 2878 B/op 42 allocs/op
BenchmarkContentsFiltered-4 200000 11222 ns/op 2498 B/op 46 allocs/op
BenchmarkChildren-4 2000000 650 ns/op 152 B/op 7 allocs/op
BenchmarkChildrenFiltered-4 500000 2568 ns/op 352 B/op 15 allocs/op
BenchmarkParent-4 2000 702513 ns/op 194478 B/op 828 allocs/op
BenchmarkParentFiltered-4 2000 690778 ns/op 194658 B/op 835 allocs/op
BenchmarkParents-4 10000 124855 ns/op 49869 B/op 868 allocs/op
BenchmarkParentsFiltered-4 10000 128535 ns/op 50456 B/op 876 allocs/op
BenchmarkParentsUntil-4 20000 72982 ns/op 23802 B/op 388 allocs/op
BenchmarkParentsUntilSelection-4 10000 156099 ns/op 72453 B/op 1549 allocs/op
BenchmarkParentsUntilNodes-4 10000 156610 ns/op 72455 B/op 1549 allocs/op
BenchmarkParentsFilteredUntil-4 100000 15549 ns/op 4068 B/op 94 allocs/op
BenchmarkParentsFilteredUntilSelection-4 100000 20564 ns/op 7276 B/op 198 allocs/op
BenchmarkParentsFilteredUntilNodes-4 100000 20635 ns/op 7276 B/op 198 allocs/op
BenchmarkSiblings-4 3000 565114 ns/op 205910 B/op 336 allocs/op
BenchmarkSiblingsFiltered-4 3000 580264 ns/op 206993 B/op 345 allocs/op
BenchmarkNext-4 20000 93177 ns/op 26810 B/op 169 allocs/op
BenchmarkNextFiltered-4 20000 94171 ns/op 27013 B/op 175 allocs/op
BenchmarkNextAll-4 5000 270320 ns/op 89289 B/op 237 allocs/op
BenchmarkNextAllFiltered-4 5000 275283 ns/op 90375 B/op 246 allocs/op
BenchmarkPrev-4 20000 92777 ns/op 26810 B/op 169 allocs/op
BenchmarkPrevFiltered-4 20000 95577 ns/op 27007 B/op 175 allocs/op
BenchmarkPrevAll-4 20000 86339 ns/op 27515 B/op 151 allocs/op
BenchmarkPrevAllFiltered-4 20000 87759 ns/op 27715 B/op 157 allocs/op
BenchmarkNextUntil-4 10000 163930 ns/op 48541 B/op 330 allocs/op
BenchmarkNextUntilSelection-4 30000 56382 ns/op 23880 B/op 556 allocs/op
BenchmarkNextUntilNodes-4 100000 18883 ns/op 8703 B/op 252 allocs/op
BenchmarkPrevUntil-4 3000 484668 ns/op 145402 B/op 611 allocs/op
BenchmarkPrevUntilSelection-4 20000 72125 ns/op 28865 B/op 705 allocs/op
BenchmarkPrevUntilNodes-4 100000 14722 ns/op 6510 B/op 205 allocs/op
BenchmarkNextFilteredUntil-4 50000 39006 ns/op 10990 B/op 192 allocs/op
BenchmarkNextFilteredUntilSelection-4 20000 66048 ns/op 25641 B/op 586 allocs/op
BenchmarkNextFilteredUntilNodes-4 20000 65314 ns/op 25640 B/op 586 allocs/op
BenchmarkPrevFilteredUntil-4 50000 33312 ns/op 9709 B/op 189 allocs/op
BenchmarkPrevFilteredUntilSelection-4 20000 64197 ns/op 24981 B/op 601 allocs/op
BenchmarkPrevFilteredUntilNodes-4 20000 64505 ns/op 24982 B/op 601 allocs/op
BenchmarkClosest-4 500000 4065 ns/op 160 B/op 8 allocs/op
BenchmarkClosestSelection-4 2000000 756 ns/op 96 B/op 6 allocs/op
BenchmarkClosestNodes-4 2000000 753 ns/op 96 B/op 6 allocs/op
PASS
ok github.com/PuerkitoBio/goquery 162.053s
+85
View File
@@ -0,0 +1,85 @@
BenchmarkFirst-4 30000000 51.8 ns/op 48 B/op 1 allocs/op
BenchmarkLast-4 30000000 50.1 ns/op 48 B/op 1 allocs/op
BenchmarkEq-4 30000000 51.4 ns/op 48 B/op 1 allocs/op
BenchmarkSlice-4 500000000 3.52 ns/op 0 B/op 0 allocs/op
BenchmarkGet-4 2000000000 1.65 ns/op 0 B/op 0 allocs/op
BenchmarkIndex-4 2000000 787 ns/op 248 B/op 10 allocs/op
BenchmarkIndexSelector-4 100000 16952 ns/op 3839 B/op 21 allocs/op
BenchmarkIndexOfNode-4 200000000 6.42 ns/op 0 B/op 0 allocs/op
BenchmarkIndexOfSelection-4 200000000 7.12 ns/op 0 B/op 0 allocs/op
BenchmarkMetalReviewExample-4 10000 141994 ns/op 12418 B/op 320 allocs/op
BenchmarkAdd-4 200000 10367 ns/op 208 B/op 9 allocs/op
BenchmarkAddSelection-4 10000000 152 ns/op 48 B/op 1 allocs/op
BenchmarkAddNodes-4 10000000 147 ns/op 48 B/op 1 allocs/op
BenchmarkAndSelf-4 1000000 1647 ns/op 1008 B/op 5 allocs/op
BenchmarkFilter-4 100000 19522 ns/op 360 B/op 8 allocs/op
BenchmarkNot-4 100000 22546 ns/op 136 B/op 5 allocs/op
BenchmarkFilterFunction-4 50000 35087 ns/op 22976 B/op 755 allocs/op
BenchmarkNotFunction-4 50000 39123 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterNodes-4 50000 34890 ns/op 20960 B/op 749 allocs/op
BenchmarkNotNodes-4 30000 41145 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterSelection-4 50000 33735 ns/op 20960 B/op 749 allocs/op
BenchmarkNotSelection-4 30000 41334 ns/op 29120 B/op 757 allocs/op
BenchmarkHas-4 5000 264058 ns/op 2370 B/op 50 allocs/op
BenchmarkHasNodes-4 10000 151718 ns/op 21184 B/op 752 allocs/op
BenchmarkHasSelection-4 10000 156955 ns/op 21184 B/op 752 allocs/op
BenchmarkEnd-4 2000000000 1.01 ns/op 0 B/op 0 allocs/op
BenchmarkEach-4 300000 4660 ns/op 3304 B/op 118 allocs/op
BenchmarkMap-4 200000 8404 ns/op 5572 B/op 184 allocs/op
BenchmarkEachWithBreak-4 2000000 806 ns/op 560 B/op 20 allocs/op
BenchmarkAttr-4 100000000 21.6 ns/op 0 B/op 0 allocs/op
BenchmarkText-4 200000 8911 ns/op 7536 B/op 110 allocs/op
BenchmarkLength-4 2000000000 0.34 ns/op 0 B/op 0 allocs/op
BenchmarkHtml-4 3000000 405 ns/op 120 B/op 2 allocs/op
BenchmarkIs-4 100000 22228 ns/op 88 B/op 4 allocs/op
BenchmarkIsPositional-4 50000 26469 ns/op 1112 B/op 10 allocs/op
BenchmarkIsFunction-4 1000000 1240 ns/op 784 B/op 28 allocs/op
BenchmarkIsSelection-4 50000 33709 ns/op 20960 B/op 749 allocs/op
BenchmarkIsNodes-4 50000 33711 ns/op 20960 B/op 749 allocs/op
BenchmarkHasClass-4 10000 236005 ns/op 14944 B/op 976 allocs/op
BenchmarkContains-4 200000000 7.47 ns/op 0 B/op 0 allocs/op
BenchmarkFind-4 100000 16075 ns/op 3839 B/op 21 allocs/op
BenchmarkFindWithinSelection-4 30000 41418 ns/op 3539 B/op 82 allocs/op
BenchmarkFindSelection-4 10000 209490 ns/op 5616 B/op 89 allocs/op
BenchmarkFindNodes-4 10000 208206 ns/op 5614 B/op 89 allocs/op
BenchmarkContents-4 300000 4751 ns/op 1420 B/op 36 allocs/op
BenchmarkContentsFiltered-4 300000 5454 ns/op 1570 B/op 41 allocs/op
BenchmarkChildren-4 3000000 527 ns/op 152 B/op 7 allocs/op
BenchmarkChildrenFiltered-4 1000000 2484 ns/op 352 B/op 15 allocs/op
BenchmarkParent-4 50000 34724 ns/op 6940 B/op 387 allocs/op
BenchmarkParentFiltered-4 50000 35596 ns/op 7141 B/op 394 allocs/op
BenchmarkParents-4 20000 62094 ns/op 30720 B/op 837 allocs/op
BenchmarkParentsFiltered-4 20000 63223 ns/op 31304 B/op 845 allocs/op
BenchmarkParentsUntil-4 50000 30391 ns/op 11828 B/op 358 allocs/op
BenchmarkParentsUntilSelection-4 20000 99962 ns/op 54075 B/op 1523 allocs/op
BenchmarkParentsUntilNodes-4 20000 98763 ns/op 54073 B/op 1523 allocs/op
BenchmarkParentsFilteredUntil-4 200000 7982 ns/op 2787 B/op 88 allocs/op
BenchmarkParentsFilteredUntilSelection-4 100000 13618 ns/op 5995 B/op 192 allocs/op
BenchmarkParentsFilteredUntilNodes-4 100000 13639 ns/op 5994 B/op 192 allocs/op
BenchmarkSiblings-4 20000 75287 ns/op 28453 B/op 225 allocs/op
BenchmarkSiblingsFiltered-4 20000 80139 ns/op 29543 B/op 234 allocs/op
BenchmarkNext-4 100000 14270 ns/op 4659 B/op 117 allocs/op
BenchmarkNextFiltered-4 100000 15352 ns/op 4860 B/op 123 allocs/op
BenchmarkNextAll-4 20000 60811 ns/op 22771 B/op 157 allocs/op
BenchmarkNextAllFiltered-4 20000 69079 ns/op 23871 B/op 166 allocs/op
BenchmarkPrev-4 100000 14417 ns/op 4659 B/op 117 allocs/op
BenchmarkPrevFiltered-4 100000 15443 ns/op 4859 B/op 123 allocs/op
BenchmarkPrevAll-4 100000 22008 ns/op 7346 B/op 120 allocs/op
BenchmarkPrevAllFiltered-4 100000 23212 ns/op 7544 B/op 126 allocs/op
BenchmarkNextUntil-4 50000 30589 ns/op 8767 B/op 267 allocs/op
BenchmarkNextUntilSelection-4 30000 40875 ns/op 19862 B/op 546 allocs/op
BenchmarkNextUntilNodes-4 100000 15987 ns/op 8134 B/op 249 allocs/op
BenchmarkPrevUntil-4 20000 98799 ns/op 25727 B/op 467 allocs/op
BenchmarkPrevUntilSelection-4 30000 51874 ns/op 24875 B/op 694 allocs/op
BenchmarkPrevUntilNodes-4 100000 12901 ns/op 6334 B/op 204 allocs/op
BenchmarkNextFilteredUntil-4 100000 19869 ns/op 5909 B/op 177 allocs/op
BenchmarkNextFilteredUntilSelection-4 30000 45412 ns/op 20557 B/op 571 allocs/op
BenchmarkNextFilteredUntilNodes-4 30000 45363 ns/op 20557 B/op 571 allocs/op
BenchmarkPrevFilteredUntil-4 100000 19357 ns/op 6033 B/op 179 allocs/op
BenchmarkPrevFilteredUntilSelection-4 30000 46396 ns/op 21305 B/op 591 allocs/op
BenchmarkPrevFilteredUntilNodes-4 30000 46133 ns/op 21305 B/op 591 allocs/op
BenchmarkClosest-4 500000 3448 ns/op 160 B/op 8 allocs/op
BenchmarkClosestSelection-4 3000000 528 ns/op 96 B/op 6 allocs/op
BenchmarkClosestNodes-4 3000000 523 ns/op 96 B/op 6 allocs/op
PASS
ok github.com/PuerkitoBio/goquery 162.012s
+86
View File
@@ -0,0 +1,86 @@
BenchmarkFirst-4 30000000 51.7 ns/op 48 B/op 1 allocs/op
BenchmarkLast-4 30000000 51.9 ns/op 48 B/op 1 allocs/op
BenchmarkEq-4 30000000 50.0 ns/op 48 B/op 1 allocs/op
BenchmarkSlice-4 500000000 3.47 ns/op 0 B/op 0 allocs/op
BenchmarkGet-4 2000000000 1.68 ns/op 0 B/op 0 allocs/op
BenchmarkIndex-4 2000000 804 ns/op 248 B/op 10 allocs/op
BenchmarkIndexSelector-4 100000 16285 ns/op 3839 B/op 21 allocs/op
BenchmarkIndexOfNode-4 200000000 6.50 ns/op 0 B/op 0 allocs/op
BenchmarkIndexOfSelection-4 200000000 7.02 ns/op 0 B/op 0 allocs/op
BenchmarkMetalReviewExample-4 10000 143160 ns/op 12417 B/op 320 allocs/op
BenchmarkAdd-4 200000 10326 ns/op 208 B/op 9 allocs/op
BenchmarkAddSelection-4 10000000 155 ns/op 48 B/op 1 allocs/op
BenchmarkAddNodes-4 10000000 156 ns/op 48 B/op 1 allocs/op
BenchmarkAddNodesBig-4 20000 94439 ns/op 21847 B/op 37 allocs/op
BenchmarkAndSelf-4 1000000 1791 ns/op 1008 B/op 5 allocs/op
BenchmarkFilter-4 100000 19470 ns/op 360 B/op 8 allocs/op
BenchmarkNot-4 100000 22500 ns/op 136 B/op 5 allocs/op
BenchmarkFilterFunction-4 50000 34578 ns/op 22976 B/op 755 allocs/op
BenchmarkNotFunction-4 50000 38703 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterNodes-4 50000 34486 ns/op 20960 B/op 749 allocs/op
BenchmarkNotNodes-4 30000 41094 ns/op 29120 B/op 757 allocs/op
BenchmarkFilterSelection-4 50000 33623 ns/op 20960 B/op 749 allocs/op
BenchmarkNotSelection-4 30000 41483 ns/op 29120 B/op 757 allocs/op
BenchmarkHas-4 5000 266628 ns/op 2371 B/op 50 allocs/op
BenchmarkHasNodes-4 10000 152617 ns/op 21184 B/op 752 allocs/op
BenchmarkHasSelection-4 10000 156682 ns/op 21184 B/op 752 allocs/op
BenchmarkEnd-4 2000000000 1.00 ns/op 0 B/op 0 allocs/op
BenchmarkEach-4 300000 4712 ns/op 3304 B/op 118 allocs/op
BenchmarkMap-4 200000 8434 ns/op 5572 B/op 184 allocs/op
BenchmarkEachWithBreak-4 2000000 819 ns/op 560 B/op 20 allocs/op
BenchmarkAttr-4 100000000 21.7 ns/op 0 B/op 0 allocs/op
BenchmarkText-4 200000 9376 ns/op 7536 B/op 110 allocs/op
BenchmarkLength-4 2000000000 0.35 ns/op 0 B/op 0 allocs/op
BenchmarkHtml-4 5000000 401 ns/op 120 B/op 2 allocs/op
BenchmarkIs-4 100000 22214 ns/op 88 B/op 4 allocs/op
BenchmarkIsPositional-4 50000 26559 ns/op 1112 B/op 10 allocs/op
BenchmarkIsFunction-4 1000000 1228 ns/op 784 B/op 28 allocs/op
BenchmarkIsSelection-4 50000 33471 ns/op 20960 B/op 749 allocs/op
BenchmarkIsNodes-4 50000 34461 ns/op 20960 B/op 749 allocs/op
BenchmarkHasClass-4 10000 232429 ns/op 14944 B/op 976 allocs/op
BenchmarkContains-4 200000000 7.62 ns/op 0 B/op 0 allocs/op
BenchmarkFind-4 100000 16114 ns/op 3839 B/op 21 allocs/op
BenchmarkFindWithinSelection-4 30000 42520 ns/op 3540 B/op 82 allocs/op
BenchmarkFindSelection-4 10000 209801 ns/op 5615 B/op 89 allocs/op
BenchmarkFindNodes-4 10000 209082 ns/op 5614 B/op 89 allocs/op
BenchmarkContents-4 300000 4836 ns/op 1420 B/op 36 allocs/op
BenchmarkContentsFiltered-4 200000 5495 ns/op 1570 B/op 41 allocs/op
BenchmarkChildren-4 3000000 527 ns/op 152 B/op 7 allocs/op
BenchmarkChildrenFiltered-4 500000 2499 ns/op 352 B/op 15 allocs/op
BenchmarkParent-4 50000 34072 ns/op 6942 B/op 387 allocs/op
BenchmarkParentFiltered-4 50000 36077 ns/op 7141 B/op 394 allocs/op
BenchmarkParents-4 20000 64118 ns/op 30719 B/op 837 allocs/op
BenchmarkParentsFiltered-4 20000 63432 ns/op 31303 B/op 845 allocs/op
BenchmarkParentsUntil-4 50000 29589 ns/op 11829 B/op 358 allocs/op
BenchmarkParentsUntilSelection-4 10000 101033 ns/op 54076 B/op 1523 allocs/op
BenchmarkParentsUntilNodes-4 10000 100584 ns/op 54076 B/op 1523 allocs/op
BenchmarkParentsFilteredUntil-4 200000 8061 ns/op 2787 B/op 88 allocs/op
BenchmarkParentsFilteredUntilSelection-4 100000 13848 ns/op 5995 B/op 192 allocs/op
BenchmarkParentsFilteredUntilNodes-4 100000 13766 ns/op 5995 B/op 192 allocs/op
BenchmarkSiblings-4 20000 75135 ns/op 28453 B/op 225 allocs/op
BenchmarkSiblingsFiltered-4 20000 80532 ns/op 29544 B/op 234 allocs/op
BenchmarkNext-4 100000 14200 ns/op 4660 B/op 117 allocs/op
BenchmarkNextFiltered-4 100000 15284 ns/op 4859 B/op 123 allocs/op
BenchmarkNextAll-4 20000 60889 ns/op 22774 B/op 157 allocs/op
BenchmarkNextAllFiltered-4 20000 65125 ns/op 23869 B/op 166 allocs/op
BenchmarkPrev-4 100000 14448 ns/op 4659 B/op 117 allocs/op
BenchmarkPrevFiltered-4 100000 15444 ns/op 4859 B/op 123 allocs/op
BenchmarkPrevAll-4 100000 22019 ns/op 7344 B/op 120 allocs/op
BenchmarkPrevAllFiltered-4 100000 23307 ns/op 7545 B/op 126 allocs/op
BenchmarkNextUntil-4 50000 30287 ns/op 8766 B/op 267 allocs/op
BenchmarkNextUntilSelection-4 30000 41476 ns/op 19862 B/op 546 allocs/op
BenchmarkNextUntilNodes-4 100000 16106 ns/op 8133 B/op 249 allocs/op
BenchmarkPrevUntil-4 20000 98951 ns/op 25728 B/op 467 allocs/op
BenchmarkPrevUntilSelection-4 30000 52390 ns/op 24875 B/op 694 allocs/op
BenchmarkPrevUntilNodes-4 100000 12986 ns/op 6334 B/op 204 allocs/op
BenchmarkNextFilteredUntil-4 100000 19365 ns/op 5908 B/op 177 allocs/op
BenchmarkNextFilteredUntilSelection-4 30000 45334 ns/op 20555 B/op 571 allocs/op
BenchmarkNextFilteredUntilNodes-4 30000 45292 ns/op 20556 B/op 571 allocs/op
BenchmarkPrevFilteredUntil-4 100000 19412 ns/op 6032 B/op 179 allocs/op
BenchmarkPrevFilteredUntilSelection-4 30000 46286 ns/op 21304 B/op 591 allocs/op
BenchmarkPrevFilteredUntilNodes-4 30000 46554 ns/op 21305 B/op 591 allocs/op
BenchmarkClosest-4 500000 3480 ns/op 160 B/op 8 allocs/op
BenchmarkClosestSelection-4 2000000 722 ns/op 96 B/op 6 allocs/op
BenchmarkClosestNodes-4 2000000 719 ns/op 96 B/op 6 allocs/op
PASS
ok github.com/PuerkitoBio/goquery 160.565s
+12 -4
View File
@@ -70,7 +70,9 @@ func BenchmarkIndex(b *testing.B) {
for i := 0; i < b.N; i++ {
j = sel.Index()
}
b.Logf("Index=%d", j)
if j != 3 {
b.Fatalf("want 3, got %d", j)
}
}
func BenchmarkIndexSelector(b *testing.B) {
@@ -82,7 +84,9 @@ func BenchmarkIndexSelector(b *testing.B) {
for i := 0; i < b.N; i++ {
j = sel.IndexSelector("dd")
}
b.Logf("IndexSelector=%d", j)
if j != 4 {
b.Fatalf("want 4, got %d", j)
}
}
func BenchmarkIndexOfNode(b *testing.B) {
@@ -96,7 +100,9 @@ func BenchmarkIndexOfNode(b *testing.B) {
for i := 0; i < b.N; i++ {
j = sel.IndexOfNode(n)
}
b.Logf("IndexOfNode=%d", j)
if j != 2 {
b.Fatalf("want 2, got %d", j)
}
}
func BenchmarkIndexOfSelection(b *testing.B) {
@@ -108,5 +114,7 @@ func BenchmarkIndexOfSelection(b *testing.B) {
for i := 0; i < b.N; i++ {
j = sel.IndexOfSelection(sel2)
}
b.Logf("IndexOfSelection=%d", j)
if j != 2 {
b.Fatalf("want 2, got %d", j)
}
}
+4 -6
View File
@@ -1,15 +1,15 @@
package goquery
import (
"bytes"
"fmt"
"strconv"
"strings"
"testing"
)
func BenchmarkMetalReviewExample(b *testing.B) {
var n int
var buf bytes.Buffer
var builder strings.Builder
b.StopTimer()
doc := loadDoc("metalreview.html")
@@ -27,16 +27,14 @@ func BenchmarkMetalReviewExample(b *testing.B) {
if score, e = strconv.ParseFloat(s.Find(".score").Text(), 64); e != nil {
// Not a valid float, ignore score
if n <= 4 {
buf.WriteString(fmt.Sprintf("Review %d: %s - %s.\n", i, band, title))
builder.WriteString(fmt.Sprintf("Review %d: %s - %s.\n", i, band, title))
}
} else {
// Print all, including score
if n <= 4 {
buf.WriteString(fmt.Sprintf("Review %d: %s - %s (%2.1f).\n", i, band, title, score))
builder.WriteString(fmt.Sprintf("Review %d: %s - %s (%2.1f).\n", i, band, title, score))
}
}
})
}
b.Log(buf.String())
b.Logf("MetalReviewExample=%d", n)
}
+36 -4
View File
@@ -17,7 +17,9 @@ func BenchmarkAdd(b *testing.B) {
sel.Add("h2[title]")
}
}
b.Logf("Add=%d", n)
if n != 43 {
b.Fatalf("want 43, got %d", n)
}
}
func BenchmarkAddSelection(b *testing.B) {
@@ -34,7 +36,9 @@ func BenchmarkAddSelection(b *testing.B) {
sel.AddSelection(sel2)
}
}
b.Logf("AddSelection=%d", n)
if n != 43 {
b.Fatalf("want 43, got %d", n)
}
}
func BenchmarkAddNodes(b *testing.B) {
@@ -52,7 +56,33 @@ func BenchmarkAddNodes(b *testing.B) {
sel.AddNodes(nodes...)
}
}
b.Logf("AddNodes=%d", n)
if n != 43 {
b.Fatalf("want 43, got %d", n)
}
}
func BenchmarkAddNodesBig(b *testing.B) {
var n int
doc := DocW()
sel := doc.Find("li")
// make nodes > 1000
nodes := sel.Nodes
nodes = append(nodes, nodes...)
nodes = append(nodes, nodes...)
sel = doc.Find("xyz")
b.ResetTimer()
for i := 0; i < b.N; i++ {
if n == 0 {
n = sel.AddNodes(nodes...).Length()
} else {
sel.AddNodes(nodes...)
}
}
if n != 373 {
b.Fatalf("want 373, got %d", n)
}
}
func BenchmarkAndSelf(b *testing.B) {
@@ -68,5 +98,7 @@ func BenchmarkAndSelf(b *testing.B) {
sel.AndSelf()
}
}
b.Logf("AndSelf=%d", n)
if n != 44 {
b.Fatalf("want 44, got %d", n)
}
}
+57 -12
View File
@@ -2,6 +2,8 @@ package goquery
import (
"testing"
"github.com/andybalholm/cascadia"
)
func BenchmarkFilter(b *testing.B) {
@@ -17,7 +19,9 @@ func BenchmarkFilter(b *testing.B) {
sel.Filter(".toclevel-1")
}
}
b.Logf("Filter=%d", n)
if n != 13 {
b.Fatalf("want 13, got %d", n)
}
}
func BenchmarkNot(b *testing.B) {
@@ -33,7 +37,28 @@ func BenchmarkNot(b *testing.B) {
sel.Filter(".toclevel-2")
}
}
b.Logf("Not=%d", n)
if n != 371 {
b.Fatalf("want 371, got %d", n)
}
}
func BenchmarkNotMatcher(b *testing.B) {
var n int
b.StopTimer()
sel := DocW().Find("li")
m := cascadia.MustCompile(".toclevel-2")
b.StartTimer()
for i := 0; i < b.N; i++ {
if n == 0 {
n = sel.NotMatcher(m).Length()
} else {
sel.NotMatcher(m)
}
}
if n != 371 {
b.Fatalf("want 371, got %d", n)
}
}
func BenchmarkFilterFunction(b *testing.B) {
@@ -52,7 +77,9 @@ func BenchmarkFilterFunction(b *testing.B) {
sel.FilterFunction(f)
}
}
b.Logf("FilterFunction=%d", n)
if n != 112 {
b.Fatalf("want 112, got %d", n)
}
}
func BenchmarkNotFunction(b *testing.B) {
@@ -71,7 +98,9 @@ func BenchmarkNotFunction(b *testing.B) {
sel.NotFunction(f)
}
}
b.Logf("NotFunction=%d", n)
if n != 261 {
b.Fatalf("want 261, got %d", n)
}
}
func BenchmarkFilterNodes(b *testing.B) {
@@ -89,7 +118,9 @@ func BenchmarkFilterNodes(b *testing.B) {
sel.FilterNodes(nodes...)
}
}
b.Logf("FilterNodes=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkNotNodes(b *testing.B) {
@@ -107,7 +138,9 @@ func BenchmarkNotNodes(b *testing.B) {
sel.NotNodes(nodes...)
}
}
b.Logf("NotNodes=%d", n)
if n != 360 {
b.Fatalf("want 360, got %d", n)
}
}
func BenchmarkFilterSelection(b *testing.B) {
@@ -124,7 +157,9 @@ func BenchmarkFilterSelection(b *testing.B) {
sel.FilterSelection(sel2)
}
}
b.Logf("FilterSelection=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkNotSelection(b *testing.B) {
@@ -141,7 +176,9 @@ func BenchmarkNotSelection(b *testing.B) {
sel.NotSelection(sel2)
}
}
b.Logf("NotSelection=%d", n)
if n != 360 {
b.Fatalf("want 360, got %d", n)
}
}
func BenchmarkHas(b *testing.B) {
@@ -157,7 +194,9 @@ func BenchmarkHas(b *testing.B) {
sel.Has(".editsection")
}
}
b.Logf("Has=%d", n)
if n != 13 {
b.Fatalf("want 13, got %d", n)
}
}
func BenchmarkHasNodes(b *testing.B) {
@@ -175,7 +214,9 @@ func BenchmarkHasNodes(b *testing.B) {
sel.HasNodes(nodes...)
}
}
b.Logf("HasNodes=%d", n)
if n != 15 {
b.Fatalf("want 15, got %d", n)
}
}
func BenchmarkHasSelection(b *testing.B) {
@@ -192,7 +233,9 @@ func BenchmarkHasSelection(b *testing.B) {
sel.HasSelection(sel2)
}
}
b.Logf("HasSelection=%d", n)
if n != 15 {
b.Fatalf("want 15, got %d", n)
}
}
func BenchmarkEnd(b *testing.B) {
@@ -208,5 +251,7 @@ func BenchmarkEnd(b *testing.B) {
sel.End()
}
}
b.Logf("End=%d", n)
if n != 373 {
b.Fatalf("want 373, got %d", n)
}
}
+53 -4
View File
@@ -1,6 +1,7 @@
package goquery
import (
"strconv"
"testing"
)
@@ -19,7 +20,51 @@ func BenchmarkEach(b *testing.B) {
n = tmp
}
}
b.Logf("Each=%d", n)
if n != 59 {
b.Fatalf("want 59, got %d", n)
}
}
func BenchmarkEachIter(b *testing.B) {
var tmp, n int
b.StopTimer()
sel := DocW().Find("td")
b.StartTimer()
for i := 0; i < b.N; i++ {
for range sel.EachIter() {
tmp++
}
if n == 0 {
n = tmp
}
}
if n != 59 {
b.Fatalf("want 59, got %d", n)
}
}
func BenchmarkEachIterWithBreak(b *testing.B) {
var tmp, n int
b.StopTimer()
sel := DocW().Find("td")
b.StartTimer()
for i := 0; i < b.N; i++ {
tmp = 0
for range sel.EachIter() {
tmp++
if tmp >= 10 {
break
}
}
if n == 0 {
n = tmp
}
}
if n != 10 {
b.Fatalf("want 10, got %d", n)
}
}
func BenchmarkMap(b *testing.B) {
@@ -29,7 +74,7 @@ func BenchmarkMap(b *testing.B) {
sel := DocW().Find("td")
f := func(i int, s *Selection) string {
tmp++
return string(tmp)
return strconv.Itoa(tmp)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
@@ -38,7 +83,9 @@ func BenchmarkMap(b *testing.B) {
n = tmp
}
}
b.Logf("Map=%d", n)
if n != 59 {
b.Fatalf("want 59, got %d", n)
}
}
func BenchmarkEachWithBreak(b *testing.B) {
@@ -58,5 +105,7 @@ func BenchmarkEachWithBreak(b *testing.B) {
n = tmp
}
}
b.Logf("Each=%d", n)
if n != 10 {
b.Fatalf("want 10, got %d", n)
}
}
+26 -3
View File
@@ -13,7 +13,9 @@ func BenchmarkAttr(b *testing.B) {
for i := 0; i < b.N; i++ {
s, _ = sel.Attr("id")
}
b.Logf("Attr=%s", s)
if s != "firstHeading" {
b.Fatalf("want firstHeading, got %q", s)
}
}
func BenchmarkText(b *testing.B) {
@@ -34,7 +36,9 @@ func BenchmarkLength(b *testing.B) {
for i := 0; i < b.N; i++ {
n = sel.Length()
}
b.Logf("Length=%d", n)
if n != 14 {
b.Fatalf("want 14, got %d", n)
}
}
func BenchmarkHtml(b *testing.B) {
@@ -42,6 +46,25 @@ func BenchmarkHtml(b *testing.B) {
sel := DocW().Find("h2")
b.StartTimer()
for i := 0; i < b.N; i++ {
sel.Html()
_, _ = sel.Html()
}
}
func BenchmarkAddClass(b *testing.B) {
b.StopTimer()
sel := DocW().Find("li")
b.StartTimer()
for i := 0; i < b.N; i++ {
sel.AddClass("foo bar baz")
}
}
func BenchmarkRemoveClass(b *testing.B) {
b.StopTimer()
sel := DocW().Find("li")
sel.AddClass("foo bar baz")
b.StartTimer()
for i := 0; i < b.N; i++ {
sel.RemoveClass("foo bar baz")
}
}
+21 -7
View File
@@ -13,7 +13,9 @@ func BenchmarkIs(b *testing.B) {
for i := 0; i < b.N; i++ {
y = sel.Is(".toclevel-2")
}
b.Logf("Is=%v", y)
if !y {
b.Fatal("want true")
}
}
func BenchmarkIsPositional(b *testing.B) {
@@ -25,7 +27,9 @@ func BenchmarkIsPositional(b *testing.B) {
for i := 0; i < b.N; i++ {
y = sel.Is("li:nth-child(2)")
}
b.Logf("IsPositional=%v", y)
if !y {
b.Fatal("want true")
}
}
func BenchmarkIsFunction(b *testing.B) {
@@ -40,7 +44,9 @@ func BenchmarkIsFunction(b *testing.B) {
for i := 0; i < b.N; i++ {
y = sel.IsFunction(f)
}
b.Logf("IsFunction=%v", y)
if !y {
b.Fatal("want true")
}
}
func BenchmarkIsSelection(b *testing.B) {
@@ -53,7 +59,9 @@ func BenchmarkIsSelection(b *testing.B) {
for i := 0; i < b.N; i++ {
y = sel.IsSelection(sel2)
}
b.Logf("IsSelection=%v", y)
if !y {
b.Fatal("want true")
}
}
func BenchmarkIsNodes(b *testing.B) {
@@ -67,7 +75,9 @@ func BenchmarkIsNodes(b *testing.B) {
for i := 0; i < b.N; i++ {
y = sel.IsNodes(nodes...)
}
b.Logf("IsNodes=%v", y)
if !y {
b.Fatal("want true")
}
}
func BenchmarkHasClass(b *testing.B) {
@@ -79,7 +89,9 @@ func BenchmarkHasClass(b *testing.B) {
for i := 0; i < b.N; i++ {
y = sel.HasClass("official")
}
b.Logf("HasClass=%v", y)
if !y {
b.Fatal("want true")
}
}
func BenchmarkContains(b *testing.B) {
@@ -93,5 +105,7 @@ func BenchmarkContains(b *testing.B) {
for i := 0; i < b.N; i++ {
y = sel.Contains(node)
}
b.Logf("Contains=%v", y)
if !y {
b.Fatal("want true")
}
}
+149 -43
View File
@@ -2,6 +2,8 @@ package goquery
import (
"testing"
"github.com/andybalholm/cascadia"
)
func BenchmarkFind(b *testing.B) {
@@ -15,7 +17,9 @@ func BenchmarkFind(b *testing.B) {
DocB().Find("dd")
}
}
b.Logf("Find=%d", n)
if n != 41 {
b.Fatalf("want 41, got %d", n)
}
}
func BenchmarkFindWithinSelection(b *testing.B) {
@@ -31,7 +35,9 @@ func BenchmarkFindWithinSelection(b *testing.B) {
sel.Find("a[class]")
}
}
b.Logf("FindWithinSelection=%d", n)
if n != 39 {
b.Fatalf("want 39, got %d", n)
}
}
func BenchmarkFindSelection(b *testing.B) {
@@ -48,7 +54,9 @@ func BenchmarkFindSelection(b *testing.B) {
sel.FindSelection(sel2)
}
}
b.Logf("FindSelection=%d", n)
if n != 73 {
b.Fatalf("want 73, got %d", n)
}
}
func BenchmarkFindNodes(b *testing.B) {
@@ -66,7 +74,9 @@ func BenchmarkFindNodes(b *testing.B) {
sel.FindNodes(nodes...)
}
}
b.Logf("FindNodes=%d", n)
if n != 73 {
b.Fatalf("want 73, got %d", n)
}
}
func BenchmarkContents(b *testing.B) {
@@ -82,7 +92,9 @@ func BenchmarkContents(b *testing.B) {
sel.Contents()
}
}
b.Logf("Contents=%d", n)
if n != 16 {
b.Fatalf("want 16, got %d", n)
}
}
func BenchmarkContentsFiltered(b *testing.B) {
@@ -98,7 +110,9 @@ func BenchmarkContentsFiltered(b *testing.B) {
sel.ContentsFiltered("a[href=\"#Examples\"]")
}
}
b.Logf("ContentsFiltered=%d", n)
if n != 1 {
b.Fatalf("want 1, got %d", n)
}
}
func BenchmarkChildren(b *testing.B) {
@@ -114,7 +128,9 @@ func BenchmarkChildren(b *testing.B) {
sel.Children()
}
}
b.Logf("Children=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkChildrenFiltered(b *testing.B) {
@@ -130,7 +146,9 @@ func BenchmarkChildrenFiltered(b *testing.B) {
sel.ChildrenFiltered(".editsection")
}
}
b.Logf("ChildrenFiltered=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkParent(b *testing.B) {
@@ -146,7 +164,9 @@ func BenchmarkParent(b *testing.B) {
sel.Parent()
}
}
b.Logf("Parent=%d", n)
if n != 55 {
b.Fatalf("want 55, got %d", n)
}
}
func BenchmarkParentFiltered(b *testing.B) {
@@ -162,7 +182,9 @@ func BenchmarkParentFiltered(b *testing.B) {
sel.ParentFiltered("ul[id]")
}
}
b.Logf("ParentFiltered=%d", n)
if n != 4 {
b.Fatalf("want 4, got %d", n)
}
}
func BenchmarkParents(b *testing.B) {
@@ -178,7 +200,9 @@ func BenchmarkParents(b *testing.B) {
sel.Parents()
}
}
b.Logf("Parents=%d", n)
if n != 73 {
b.Fatalf("want 73, got %d", n)
}
}
func BenchmarkParentsFiltered(b *testing.B) {
@@ -194,7 +218,9 @@ func BenchmarkParentsFiltered(b *testing.B) {
sel.ParentsFiltered("tr")
}
}
b.Logf("ParentsFiltered=%d", n)
if n != 18 {
b.Fatalf("want 18, got %d", n)
}
}
func BenchmarkParentsUntil(b *testing.B) {
@@ -210,7 +236,9 @@ func BenchmarkParentsUntil(b *testing.B) {
sel.ParentsUntil("table")
}
}
b.Logf("ParentsUntil=%d", n)
if n != 52 {
b.Fatalf("want 52, got %d", n)
}
}
func BenchmarkParentsUntilSelection(b *testing.B) {
@@ -227,7 +255,9 @@ func BenchmarkParentsUntilSelection(b *testing.B) {
sel.ParentsUntilSelection(sel2)
}
}
b.Logf("ParentsUntilSelection=%d", n)
if n != 70 {
b.Fatalf("want 70, got %d", n)
}
}
func BenchmarkParentsUntilNodes(b *testing.B) {
@@ -245,7 +275,9 @@ func BenchmarkParentsUntilNodes(b *testing.B) {
sel.ParentsUntilNodes(nodes...)
}
}
b.Logf("ParentsUntilNodes=%d", n)
if n != 70 {
b.Fatalf("want 70, got %d", n)
}
}
func BenchmarkParentsFilteredUntil(b *testing.B) {
@@ -261,7 +293,9 @@ func BenchmarkParentsFilteredUntil(b *testing.B) {
sel.ParentsFilteredUntil(":nth-child(1)", "ul")
}
}
b.Logf("ParentsFilteredUntil=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkParentsFilteredUntilSelection(b *testing.B) {
@@ -278,7 +312,9 @@ func BenchmarkParentsFilteredUntilSelection(b *testing.B) {
sel.ParentsFilteredUntilSelection(":nth-child(1)", sel2)
}
}
b.Logf("ParentsFilteredUntilSelection=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkParentsFilteredUntilNodes(b *testing.B) {
@@ -296,7 +332,9 @@ func BenchmarkParentsFilteredUntilNodes(b *testing.B) {
sel.ParentsFilteredUntilNodes(":nth-child(1)", nodes...)
}
}
b.Logf("ParentsFilteredUntilNodes=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkSiblings(b *testing.B) {
@@ -312,7 +350,9 @@ func BenchmarkSiblings(b *testing.B) {
sel.Siblings()
}
}
b.Logf("Siblings=%d", n)
if n != 293 {
b.Fatalf("want 293, got %d", n)
}
}
func BenchmarkSiblingsFiltered(b *testing.B) {
@@ -328,7 +368,9 @@ func BenchmarkSiblingsFiltered(b *testing.B) {
sel.SiblingsFiltered("[class]")
}
}
b.Logf("SiblingsFiltered=%d", n)
if n != 46 {
b.Fatalf("want 46, got %d", n)
}
}
func BenchmarkNext(b *testing.B) {
@@ -344,7 +386,9 @@ func BenchmarkNext(b *testing.B) {
sel.Next()
}
}
b.Logf("Next=%d", n)
if n != 49 {
b.Fatalf("want 49, got %d", n)
}
}
func BenchmarkNextFiltered(b *testing.B) {
@@ -360,7 +404,9 @@ func BenchmarkNextFiltered(b *testing.B) {
sel.NextFiltered("[class]")
}
}
b.Logf("NextFiltered=%d", n)
if n != 6 {
b.Fatalf("want 6, got %d", n)
}
}
func BenchmarkNextAll(b *testing.B) {
@@ -376,7 +422,9 @@ func BenchmarkNextAll(b *testing.B) {
sel.NextAll()
}
}
b.Logf("NextAll=%d", n)
if n != 234 {
b.Fatalf("want 234, got %d", n)
}
}
func BenchmarkNextAllFiltered(b *testing.B) {
@@ -392,7 +440,9 @@ func BenchmarkNextAllFiltered(b *testing.B) {
sel.NextAllFiltered("[class]")
}
}
b.Logf("NextAllFiltered=%d", n)
if n != 33 {
b.Fatalf("want 33, got %d", n)
}
}
func BenchmarkPrev(b *testing.B) {
@@ -408,7 +458,9 @@ func BenchmarkPrev(b *testing.B) {
sel.Prev()
}
}
b.Logf("Prev=%d", n)
if n != 49 {
b.Fatalf("want 49, got %d", n)
}
}
func BenchmarkPrevFiltered(b *testing.B) {
@@ -426,7 +478,9 @@ func BenchmarkPrevFiltered(b *testing.B) {
}
// There is one more Prev li with a class, compared to Next li with a class
// (confirmed by looking at the HTML, this is ok)
b.Logf("PrevFiltered=%d", n)
if n != 7 {
b.Fatalf("want 7, got %d", n)
}
}
func BenchmarkPrevAll(b *testing.B) {
@@ -442,7 +496,9 @@ func BenchmarkPrevAll(b *testing.B) {
sel.PrevAll()
}
}
b.Logf("PrevAll=%d", n)
if n != 78 {
b.Fatalf("want 78, got %d", n)
}
}
func BenchmarkPrevAllFiltered(b *testing.B) {
@@ -458,7 +514,9 @@ func BenchmarkPrevAllFiltered(b *testing.B) {
sel.PrevAllFiltered("[class]")
}
}
b.Logf("PrevAllFiltered=%d", n)
if n != 6 {
b.Fatalf("want 6, got %d", n)
}
}
func BenchmarkNextUntil(b *testing.B) {
@@ -474,7 +532,9 @@ func BenchmarkNextUntil(b *testing.B) {
sel.NextUntil(":nth-child(4)")
}
}
b.Logf("NextUntil=%d", n)
if n != 84 {
b.Fatalf("want 84, got %d", n)
}
}
func BenchmarkNextUntilSelection(b *testing.B) {
@@ -491,7 +551,9 @@ func BenchmarkNextUntilSelection(b *testing.B) {
sel.NextUntilSelection(sel2)
}
}
b.Logf("NextUntilSelection=%d", n)
if n != 42 {
b.Fatalf("want 42, got %d", n)
}
}
func BenchmarkNextUntilNodes(b *testing.B) {
@@ -509,7 +571,9 @@ func BenchmarkNextUntilNodes(b *testing.B) {
sel.NextUntilNodes(nodes...)
}
}
b.Logf("NextUntilNodes=%d", n)
if n != 12 {
b.Fatalf("want 12, got %d", n)
}
}
func BenchmarkPrevUntil(b *testing.B) {
@@ -525,7 +589,9 @@ func BenchmarkPrevUntil(b *testing.B) {
sel.PrevUntil(":nth-child(4)")
}
}
b.Logf("PrevUntil=%d", n)
if n != 238 {
b.Fatalf("want 238, got %d", n)
}
}
func BenchmarkPrevUntilSelection(b *testing.B) {
@@ -542,7 +608,9 @@ func BenchmarkPrevUntilSelection(b *testing.B) {
sel.PrevUntilSelection(sel2)
}
}
b.Logf("PrevUntilSelection=%d", n)
if n != 49 {
b.Fatalf("want 49, got %d", n)
}
}
func BenchmarkPrevUntilNodes(b *testing.B) {
@@ -560,7 +628,9 @@ func BenchmarkPrevUntilNodes(b *testing.B) {
sel.PrevUntilNodes(nodes...)
}
}
b.Logf("PrevUntilNodes=%d", n)
if n != 11 {
b.Fatalf("want 11, got %d", n)
}
}
func BenchmarkNextFilteredUntil(b *testing.B) {
@@ -576,7 +646,9 @@ func BenchmarkNextFilteredUntil(b *testing.B) {
sel.NextFilteredUntil("p", "div")
}
}
b.Logf("NextFilteredUntil=%d", n)
if n != 22 {
b.Fatalf("want 22, got %d", n)
}
}
func BenchmarkNextFilteredUntilSelection(b *testing.B) {
@@ -593,7 +665,9 @@ func BenchmarkNextFilteredUntilSelection(b *testing.B) {
sel.NextFilteredUntilSelection("p", sel2)
}
}
b.Logf("NextFilteredUntilSelection=%d", n)
if n != 22 {
b.Fatalf("want 22, got %d", n)
}
}
func BenchmarkNextFilteredUntilNodes(b *testing.B) {
@@ -611,7 +685,9 @@ func BenchmarkNextFilteredUntilNodes(b *testing.B) {
sel.NextFilteredUntilNodes("p", nodes...)
}
}
b.Logf("NextFilteredUntilNodes=%d", n)
if n != 22 {
b.Fatalf("want 22, got %d", n)
}
}
func BenchmarkPrevFilteredUntil(b *testing.B) {
@@ -627,7 +703,9 @@ func BenchmarkPrevFilteredUntil(b *testing.B) {
sel.PrevFilteredUntil("p", "div")
}
}
b.Logf("PrevFilteredUntil=%d", n)
if n != 20 {
b.Fatalf("want 20, got %d", n)
}
}
func BenchmarkPrevFilteredUntilSelection(b *testing.B) {
@@ -644,7 +722,9 @@ func BenchmarkPrevFilteredUntilSelection(b *testing.B) {
sel.PrevFilteredUntilSelection("p", sel2)
}
}
b.Logf("PrevFilteredUntilSelection=%d", n)
if n != 20 {
b.Fatalf("want 20, got %d", n)
}
}
func BenchmarkPrevFilteredUntilNodes(b *testing.B) {
@@ -662,7 +742,9 @@ func BenchmarkPrevFilteredUntilNodes(b *testing.B) {
sel.PrevFilteredUntilNodes("p", nodes...)
}
}
b.Logf("PrevFilteredUntilNodes=%d", n)
if n != 20 {
b.Fatalf("want 20, got %d", n)
}
}
func BenchmarkClosest(b *testing.B) {
@@ -678,7 +760,9 @@ func BenchmarkClosest(b *testing.B) {
sel.Closest(".pvk-content")
}
}
b.Logf("Closest=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkClosestSelection(b *testing.B) {
@@ -695,7 +779,9 @@ func BenchmarkClosestSelection(b *testing.B) {
sel.ClosestSelection(sel2)
}
}
b.Logf("ClosestSelection=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkClosestNodes(b *testing.B) {
@@ -712,5 +798,25 @@ func BenchmarkClosestNodes(b *testing.B) {
sel.ClosestNodes(nodes...)
}
}
b.Logf("ClosestNodes=%d", n)
if n != 2 {
b.Fatalf("want 2, got %d", n)
}
}
func BenchmarkSingleMatcher(b *testing.B) {
doc := Doc()
multi := cascadia.MustCompile(`div`)
single := SingleMatcher(multi)
b.ResetTimer()
b.Run("multi", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = doc.FindMatcher(multi)
}
})
b.Run("single", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = doc.FindMatcher(single)
}
})
}
+1 -1
View File
@@ -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,
+5 -5
View File
@@ -4,16 +4,16 @@
The `go.net/html` package used by `goquery` requires that the html document is UTF-8 encoded. When you know the encoding of the html page is not UTF-8, you can use the `iconv` package to convert it to UTF-8 (there are various implementation of the `iconv` API, see [godoc.org][iconv] for other options):
```
```bash
$ go get -u github.com/djimenez/iconv-go
```
and then:
```
```golang
// Load the URL
res, err := http.Get(url)
if e != nil {
if err != nil {
// handle error
}
defer res.Body.Close()
@@ -51,7 +51,7 @@ You can find a code example using `otto` [in this gist][exotto]. Thanks to githu
If all you need is a normal `for` loop over all nodes in the current selection, where `Map/Each`-style iteration is not necessary, you can use the following:
```
```golang
sel := Doc().Find(".selector")
for i := range sel.Nodes {
single := sel.Eq(i)
@@ -65,4 +65,4 @@ Thanks to github user @jmoiron.
[otto]: https://github.com/robertkrimen/otto
[exotto]: https://gist.github.com/cryptix/87127f76a94183747b53
[iconv]: http://godoc.org/?q=iconv
[text]: http://godoc.org/code.google.com/p/go.text/encoding
[text]: https://godoc.org/golang.org/x/text/encoding
+126 -10
View File
@@ -1,26 +1,38 @@
package goquery
package goquery_test
import (
"fmt"
"log"
"net/http"
"os"
"strings"
// In real use, this import would be required (not in this example, since it
// is part of the goquery package)
//"github.com/PuerkitoBio/goquery"
"github.com/PuerkitoBio/goquery"
"golang.org/x/net/html"
)
// This example scrapes the reviews shown on the home page of metalsucks.net.
func ExampleScrape_MetalSucks() {
// Load the HTML document (in real use, the type would be *goquery.Document)
doc, err := NewDocument("http://metalsucks.net")
func Example() {
// Request the HTML page.
res, err := http.Get("http://metalsucks.net")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
}
// Load the HTML document
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
log.Fatal(err)
}
// Find the review items (the type of the Selection would be *goquery.Selection)
doc.Find(".reviews-wrap article .review-rhs").Each(func(i int, s *Selection) {
// Find the review items
doc.Find(".sidebar-reviews article .content-block").Each(func(i int, s *goquery.Selection) {
// For each item found, get the band and title
band := s.Find("h3").Text()
band := s.Find("a").Text()
title := s.Find("i").Text()
fmt.Printf("Review %d: %s - %s\n", i, band, title)
})
@@ -30,3 +42,107 @@ func ExampleScrape_MetalSucks() {
// xOutput: voluntarily fail the Example output.
}
// This example shows how to use NewDocumentFromReader from a file.
func ExampleNewDocumentFromReader_file() {
// create from a file
f, err := os.Open("some/file.html")
if err != nil {
log.Fatal(err)
}
defer f.Close()
doc, err := goquery.NewDocumentFromReader(f)
if err != nil {
log.Fatal(err)
}
// use the goquery document...
_ = doc.Find("h1")
}
// This example shows how to use NewDocumentFromReader from a string.
func ExampleNewDocumentFromReader_string() {
// create from a string
data := `
<html>
<head>
<title>My document</title>
</head>
<body>
<h1>Header</h1>
</body>
</html>`
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
if err != nil {
log.Fatal(err)
}
header := doc.Find("h1").Text()
fmt.Println(header)
// Output: Header
}
func ExampleSingle() {
html := `
<html>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
</body>
</html>
`
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
log.Fatal(err)
}
// By default, the selector string selects all matching nodes
multiSel := doc.Find("div")
fmt.Println(multiSel.Text())
// Using goquery.Single, only the first match is selected
singleSel := doc.FindMatcher(goquery.Single("div"))
fmt.Println(singleSel.Text())
// Output:
// 123
// 1
}
// This example shows how to use the goquery.Text function to extract clean,
// human-readable text from a selection, similar to BeautifulSoup's get_text.
func ExampleText() {
page := `
<html>
<body>
<div id="content">
<h1> Hello </h1>
<p>world</p>
<script>var ignored = 1;</script>
</div>
</body>
</html>
`
doc, err := goquery.NewDocumentFromReader(strings.NewReader(page))
if err != nil {
log.Fatal(err)
}
// Trim each text node, join the remaining ones with a space, and skip the
// text of <script> and <style> elements.
text := goquery.Text(doc.Find("#content"), &goquery.TextOptions{
Separator: " ",
Trim: true,
Keep: func(n *html.Node) bool {
if p := n.Parent; p != nil && p.Type == html.ElementNode {
return p.Data != "script" && p.Data != "style"
}
return true
},
})
fmt.Println(text)
// Output:
// Hello world
}
+27 -6
View File
@@ -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
@@ -38,12 +35,36 @@ func (s *Selection) Union(sel *Selection) *Selection {
// AddNodes adds the specified nodes to those in the
// current selection and returns a new Selection object.
func (s *Selection) AddNodes(nodes ...*html.Node) *Selection {
return pushStack(s, appendWithoutDuplicates(s.Nodes, nodes))
return pushStack(s, appendWithoutDuplicates(s.Nodes, nodes, nil))
}
// AndSelf adds the previous set of elements on the stack to the current set.
// It returns a new Selection object containing the current Selection combined
// with the previous one.
// Deprecated: This function has been deprecated and is now an alias for AddBack().
func (s *Selection) AndSelf() *Selection {
return s.AddBack()
}
// AddBack adds the previous set of elements on the stack to the current set.
// It returns a new Selection object containing the current Selection combined
// with the previous one.
func (s *Selection) AddBack() *Selection {
return s.AddSelection(s.prevSel)
}
// AddBackFiltered reduces the previous set of elements on the stack to those that
// match the selector string, and adds them to the current set.
// It returns a new Selection object containing the current Selection combined
// with the filtered previous one
func (s *Selection) AddBackFiltered(selector string) *Selection {
return s.AddSelection(s.prevSel.Filter(selector))
}
// AddBackMatcher reduces the previous set of elements on the stack to those that match
// the matcher, and adds them to the current set.
// It returns a new Selection object containing the current Selection combined
// with the filtered previous one
func (s *Selection) AddBackMatcher(m Matcher) *Selection {
return s.AddSelection(s.prevSel.FilterMatcher(m))
}
+50
View File
@@ -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()
@@ -56,6 +66,24 @@ func TestAddNodesRollback(t *testing.T) {
assertEqual(t, sel, sel2)
}
func TestAddNodesBig(t *testing.T) {
doc := DocW()
sel := doc.Find("li")
assertLength(t, sel.Nodes, 373)
sel2 := doc.Find("xyz")
assertLength(t, sel2.Nodes, 0)
nodes := sel.Nodes
sel2 = sel2.AddNodes(nodes...)
assertLength(t, sel2.Nodes, 373)
nodes2 := append(nodes, nodes...)
sel2 = sel2.End().AddNodes(nodes2...)
assertLength(t, sel2.Nodes, 373)
nodes3 := append(nodes2, nodes...)
sel2 = sel2.End().AddNodes(nodes3...)
assertLength(t, sel2.Nodes, 373)
}
func TestAndSelf(t *testing.T) {
sel := Doc().Find(".span12").Last().AndSelf()
assertLength(t, sel.Nodes, 2)
@@ -66,3 +94,25 @@ func TestAndSelfRollback(t *testing.T) {
sel2 := sel.Find("a").AndSelf().End().End()
assertEqual(t, sel, sel2)
}
func TestAddBack(t *testing.T) {
sel := Doc().Find(".span12").Last().AddBack()
assertLength(t, sel.Nodes, 2)
}
func TestAddBackRollback(t *testing.T) {
sel := Doc().Find(".pvk-content")
sel2 := sel.Find("a").AddBack().End().End()
assertEqual(t, sel, sel2)
}
func TestAddBackFiltered(t *testing.T) {
sel := Doc().Find(".span12, .footer").Find("h1").AddBackFiltered(".footer")
assertLength(t, sel.Nodes, 2)
}
func TestAddBackFilteredRollback(t *testing.T) {
sel := Doc().Find(".span12, .footer")
sel2 := sel.Find("h1").AddBackFiltered(".footer").End().End()
assertEqual(t, sel, sel2)
}
+64 -28
View File
@@ -1,33 +1,30 @@
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))
return pushStack(s, winnow(s.Nodes, m, true))
}
// 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.
// It returns a new Selection object with the matching elements removed.
func (s *Selection) NotMatcher(m Matcher) *Selection {
return pushStack(s, winnow(s, m, false))
return pushStack(s, winnow(s.Nodes, m, false))
}
// FilterFunction reduces the set of matched elements to those that pass the function's test.
@@ -82,29 +79,46 @@ func (s *Selection) Intersection(sel *Selection) *Selection {
// that matches the selector.
// It returns a new Selection object with the matching elements.
func (s *Selection) Has(selector string) *Selection {
return s.HasSelection(s.document.Find(selector))
return s.HasMatcher(compileMatcher(selector))
}
// HasMatcher reduces the set of matched elements to those that have a descendant
// that matches the matcher.
// It returns a new Selection object with the matching elements.
func (s *Selection) HasMatcher(m Matcher) *Selection {
return s.HasSelection(s.document.FindMatcher(m))
result := make([]*html.Node, 0, len(s.Nodes))
// Manually create a singleMatcher (rather than using SingleMatcher, which
// returns a Matcher interface) so we can call its MatchFirst method
// directly. MatchFirst probes for the first match in a subtree without
// building a result slice, avoiding a throwaway one-element slice
// allocation for every matching child subtree.
sm := singleMatcher{m}
for _, n := range s.Nodes {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && sm.MatchFirst(c) != nil {
result = append(result, n)
break
}
}
}
return pushStack(s, result)
}
// HasNodes reduces the set of matched elements to those that have a
// descendant that matches one of the nodes.
// It returns a new Selection object with the matching elements.
func (s *Selection) HasNodes(nodes ...*html.Node) *Selection {
return s.FilterFunction(func(_ int, sel *Selection) bool {
// Add all nodes that contain one of the specified nodes
for _, n := range nodes {
if sel.Contains(n) {
return true
var result []*html.Node
for _, n := range s.Nodes {
for _, candidate := range nodes {
if nodeContains(n, candidate) {
result = append(result, n)
break
}
}
return false
})
}
return pushStack(s, result)
}
// HasSelection reduces the set of matched elements to those that have a
@@ -128,23 +142,45 @@ func (s *Selection) End() *Selection {
// Filter based on the matcher, and the indicator to keep (Filter) or
// to get rid of (Not) the matching elements.
func winnow(sel *Selection, m Matcher, keep bool) []*html.Node {
func winnow(nodes []*html.Node, m Matcher, keep bool) []*html.Node {
// Optimize if keep is requested
if keep {
return m.Filter(sel.Nodes)
return m.Filter(nodes)
}
// Use grep
return grep(sel, func(i int, s *Selection) bool {
return !m.Match(s.Get(0))
})
// Not path: call Match directly on each node, no Selection wrapper needed
result := make([]*html.Node, 0, len(nodes))
for _, n := range nodes {
if !m.Match(n) {
result = append(result, n)
}
}
return result
}
// Filter based on an array of nodes, and the indicator to keep (Filter) or
// to get rid of (Not) the matching elements.
func winnowNodes(sel *Selection, nodes []*html.Node, keep bool) []*html.Node {
return grep(sel, func(i int, s *Selection) bool {
return isInSlice(nodes, s.Get(0)) == keep
})
if len(nodes)+len(sel.Nodes) < minNodesForSet {
var result []*html.Node
for _, n := range sel.Nodes {
if isInSlice(nodes, n) == keep {
result = append(result, n)
}
}
return result
}
set := make(map[*html.Node]bool, len(nodes))
for _, n := range nodes {
set[n] = true
}
var result []*html.Node
for _, n := range sel.Nodes {
if set[n] == keep {
result = append(result, n)
}
}
return result
}
// Filter based on a function test, and the indicator to keep (Filter) or
+15
View File
@@ -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()
+8
View File
@@ -0,0 +1,8 @@
module github.com/PuerkitoBio/goquery
require (
github.com/andybalholm/cascadia v1.3.4
golang.org/x/net v0.58.0
)
go 1.25.0
+4
View File
@@ -0,0 +1,4 @@
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
+31 -3
View File
@@ -1,7 +1,12 @@
package goquery
import "iter"
// Each iterates over a Selection object, executing a function for each
// matched element. It returns the current Selection object.
// matched element. It returns the current Selection object. The function
// f is called for each element in the selection with the index of the
// element in that selection starting at 0, and a *Selection that contains
// only that element.
func (s *Selection) Each(f func(int, *Selection)) *Selection {
for i, n := range s.Nodes {
f(i, newSingleSelection(n, s.document))
@@ -9,6 +14,18 @@ func (s *Selection) Each(f func(int, *Selection)) *Selection {
return s
}
// EachIter returns an iterator that yields the Selection object in order.
// The implementation is similar to Each, but it returns an iterator instead.
func (s *Selection) EachIter() iter.Seq2[int, *Selection] {
return func(yield func(int, *Selection) bool) {
for i, n := range s.Nodes {
if !yield(i, newSingleSelection(n, s.document)) {
return
}
}
}
}
// EachWithBreak iterates over a Selection object, executing a function for each
// matched element. It is identical to Each except that it is possible to break
// out of the loop by returning false in the callback function. It returns the
@@ -23,10 +40,21 @@ func (s *Selection) EachWithBreak(f func(int, *Selection) bool) *Selection {
}
// Map passes each element in the current matched set through a function,
// producing a slice of string holding the returned values.
// producing a slice of string holding the returned values. The function
// f is called for each element in the selection with the index of the
// element in that selection starting at 0, and a *Selection that contains
// only that element.
func (s *Selection) Map(f func(int, *Selection) string) (result []string) {
return Map(s, f)
}
// Map is the generic version of Selection.Map, allowing any type to be
// returned.
func Map[E any](s *Selection, f func(int, *Selection) E) (result []E) {
result = make([]E, len(s.Nodes))
for i, n := range s.Nodes {
result = append(result, f(i, newSingleSelection(n, s.document)))
result[i] = f(i, newSingleSelection(n, s.document))
}
return result
+55
View File
@@ -86,3 +86,58 @@ func TestForRange(t *testing.T) {
t.Errorf("expected initial selection to still have length %d, got %d", initLen, sel.Length())
}
}
func TestGenericMap(t *testing.T) {
sel := Doc().Find(".pvk-content")
vals := Map(sel, func(i int, s *Selection) *html.NodeType {
n := s.Get(0)
if n.Type == html.ElementNode {
return &n.Type
}
return nil
})
for _, v := range vals {
if v == nil || *v != html.ElementNode {
t.Error("Expected Map array result to be all 'div's.")
}
}
if len(vals) != 3 {
t.Errorf("Expected Map array result to have a length of 3, found %v.", len(vals))
}
}
func TestEachIter(t *testing.T) {
var cnt int
sel := Doc().Find(".hero-unit .row-fluid")
for i, s := range sel.EachIter() {
cnt++
t.Logf("At index %v, node %v", i, s.Nodes[0].Data)
}
sel = sel.Find("a")
if cnt != 4 {
t.Errorf("Expected Each() to call function 4 times, got %v times.", cnt)
}
assertLength(t, sel.Nodes, 6)
}
func TestEachIterWithBreak(t *testing.T) {
var cnt int
sel := Doc().Find(".hero-unit .row-fluid")
for i, s := range sel.EachIter() {
cnt++
t.Logf("At index %v, node %v", i, s.Nodes[0].Data)
break
}
sel = sel.Find("a")
if cnt != 1 {
t.Errorf("Expected Each() to call function 1 time, got %v times.", cnt)
}
assertLength(t, sel.Nodes, 6)
}
+161 -32
View File
@@ -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
@@ -40,8 +39,15 @@ func (s *Selection) AfterSelection(sel *Selection) *Selection {
// AfterHtml parses the html and inserts it after the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AfterHtml(html string) *Selection {
return s.AfterNodes(parseHtml(html)...)
func (s *Selection) AfterHtml(htmlStr string) *Selection {
return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
nextSibling := node.NextSibling
for _, n := range nodes {
if node.Parent != nil {
node.Parent.InsertBefore(n, nextSibling)
}
}
})
}
// AfterNodes inserts the nodes after each element in the set of matched elements.
@@ -66,7 +72,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
@@ -86,8 +92,12 @@ func (s *Selection) AppendSelection(sel *Selection) *Selection {
}
// AppendHtml parses the html and appends it to the set of matched elements.
func (s *Selection) AppendHtml(html string) *Selection {
return s.AppendNodes(parseHtml(html)...)
func (s *Selection) AppendHtml(htmlStr string) *Selection {
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
for _, n := range nodes {
node.AppendChild(n)
}
})
}
// AppendNodes appends the specified nodes to each node in the set of matched elements.
@@ -103,7 +113,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.
@@ -124,8 +134,14 @@ func (s *Selection) BeforeSelection(sel *Selection) *Selection {
// BeforeHtml parses the html and inserts it before the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) BeforeHtml(html string) *Selection {
return s.BeforeNodes(parseHtml(html)...)
func (s *Selection) BeforeHtml(htmlStr string) *Selection {
return s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
for _, n := range nodes {
if node.Parent != nil {
node.Parent.InsertBefore(n, node)
}
}
})
}
// BeforeNodes inserts the nodes before each element in the set of matched elements.
@@ -150,8 +166,16 @@ func (s *Selection) Clone() *Selection {
// Empty removes all children nodes from the set of matched elements.
// It returns the children nodes in a new Selection.
func (s *Selection) Empty() *Selection {
var nodes []*html.Node
// Count the children first so nodes can be presized, avoiding append's
// repeated reallocations.
count := 0
for _, n := range s.Nodes {
for c := n.FirstChild; c != nil; c = c.NextSibling {
count++
}
}
nodes := make([]*html.Node, 0, count)
for _, n := range s.Nodes {
for c := n.FirstChild; c != nil; c = n.FirstChild {
n.RemoveChild(c)
@@ -165,7 +189,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
@@ -185,8 +209,13 @@ func (s *Selection) PrependSelection(sel *Selection) *Selection {
}
// PrependHtml parses the html and prepends it to the set of matched elements.
func (s *Selection) PrependHtml(html string) *Selection {
return s.PrependNodes(parseHtml(html)...)
func (s *Selection) PrependHtml(htmlStr string) *Selection {
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
firstChild := node.FirstChild
for _, n := range nodes {
node.InsertBefore(n, firstChild)
}
})
}
// PrependNodes prepends the specified nodes to each node in the set of
@@ -213,14 +242,19 @@ func (s *Selection) Remove() *Selection {
return s
}
// RemoveFiltered removes the set of matched elements by selector.
// It returns the Selection of removed nodes.
// RemoveFiltered removes from the current set of matched elements those that
// match the selector filter. It returns the Selection of removed nodes.
//
// For example if the selection s contains "<h1>", "<h2>" and "<h3>"
// and s.RemoveFiltered("h2") is called, only the "<h2>" node is removed
// (and returned), while "<h1>" and "<h3>" are kept in the document.
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.
// It returns the Selection of removed nodes.
// RemoveMatcher removes from the current set of matched elements those that
// match the Matcher filter. It returns the Selection of removed nodes.
// See RemoveFiltered for additional information.
func (s *Selection) RemoveMatcher(m Matcher) *Selection {
return s.FilterMatcher(m).Remove()
}
@@ -231,7 +265,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
@@ -257,8 +291,16 @@ func (s *Selection) ReplaceWithSelection(sel *Selection) *Selection {
// It returns the removed elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) ReplaceWithHtml(html string) *Selection {
return s.ReplaceWithNodes(parseHtml(html)...)
func (s *Selection) ReplaceWithHtml(htmlStr string) *Selection {
s.eachNodeHtml(htmlStr, true, func(node *html.Node, nodes []*html.Node) {
nextSibling := node.NextSibling
for _, n := range nodes {
if node.Parent != nil {
node.Parent.InsertBefore(n, nextSibling)
}
}
})
return s.Remove()
}
// ReplaceWithNodes replaces each element in the set of matched elements with
@@ -271,6 +313,27 @@ func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection {
return s.Remove()
}
// SetHtml sets the html content of each element in the selection to
// specified html string.
func (s *Selection) SetHtml(htmlStr string) *Selection {
for _, context := range s.Nodes {
for c := context.FirstChild; c != nil; c = context.FirstChild {
context.RemoveChild(c)
}
}
return s.eachNodeHtml(htmlStr, false, func(node *html.Node, nodes []*html.Node) {
for _, n := range nodes {
node.AppendChild(n)
}
})
}
// SetText sets the content of each element in the selection to specified content.
// The provided text string is escaped.
func (s *Selection) SetText(text string) *Selection {
return s.SetHtml(html.EscapeString(text))
}
// Unwrap removes the parents of the set of matched elements, leaving the matched
// elements (and their siblings, if any) in their place.
// It returns the original selection.
@@ -293,7 +356,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
@@ -318,8 +381,17 @@ func (s *Selection) WrapSelection(sel *Selection) *Selection {
// most child of the given HTML.
//
// It returns the original set of elements.
func (s *Selection) WrapHtml(html string) *Selection {
return s.wrapNodes(parseHtml(html)...)
func (s *Selection) WrapHtml(htmlStr string) *Selection {
cache := make(map[string][]*html.Node)
for _, n := range s.Nodes {
parent := n.Parent
if parent == nil {
parent = &html.Node{Type: html.ElementNode}
}
nodes := cachedParseHtmlWithContext(cache, htmlStr, parent)
newSingleSelection(n, s.document).wrapAllNodes(cloneNodes(nodes)...)
}
return s
}
// WrapNode wraps each element in the set of matched elements inside the inner-
@@ -345,7 +417,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,
@@ -371,8 +443,18 @@ func (s *Selection) WrapAllSelection(sel *Selection) *Selection {
// document.
//
// It returns the original set of elements.
func (s *Selection) WrapAllHtml(html string) *Selection {
return s.wrapAllNodes(parseHtml(html)...)
func (s *Selection) WrapAllHtml(htmlStr string) *Selection {
var context *html.Node
var nodes []*html.Node
if len(s.Nodes) > 0 {
context = s.Nodes[0]
if context.Parent != nil {
nodes = parseHtmlWithContext(htmlStr, context)
} else {
nodes = parseHtml(htmlStr)
}
}
return s.wrapAllNodes(nodes...)
}
func (s *Selection) wrapAllNodes(ns ...*html.Node) *Selection {
@@ -415,7 +497,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,
@@ -441,8 +523,13 @@ func (s *Selection) WrapInnerSelection(sel *Selection) *Selection {
// cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapInnerHtml(html string) *Selection {
return s.wrapInnerNodes(parseHtml(html)...)
func (s *Selection) WrapInnerHtml(htmlStr string) *Selection {
cache := make(map[string][]*html.Node)
for _, n := range s.Nodes {
nodes := cachedParseHtmlWithContext(cache, htmlStr, n)
newSingleSelection(n, s.document).wrapInnerNodes(cloneNodes(nodes)...)
}
return s
}
// WrapInnerNode wraps an HTML structure, matched by the given selector, around
@@ -473,9 +560,13 @@ func (s *Selection) wrapInnerNodes(ns ...*html.Node) *Selection {
}
func parseHtml(h string) []*html.Node {
return parseHtmlWithContext(h, &html.Node{Type: html.ElementNode})
}
func parseHtmlWithContext(h string, context *html.Node) []*html.Node {
// Errors are only returned when the io.Reader returns any error besides
// EOF, but strings.Reader never will
nodes, err := html.ParseFragment(strings.NewReader(h), &html.Node{Type: html.ElementNode})
nodes, err := html.ParseFragment(strings.NewReader(h), context)
if err != nil {
panic("goquery: failed to parse HTML: " + err.Error())
}
@@ -549,3 +640,41 @@ func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool,
return s
}
// eachNodeHtml parses the given html string and inserts the resulting nodes in the dom with the mergeFn.
// The parsed nodes are inserted for each element of the selection.
// isParent can be used to indicate that the elements of the selection should be treated as the parent for the parsed html.
// A cache is used to avoid parsing the html multiple times should the elements of the selection result in the same context.
func (s *Selection) eachNodeHtml(htmlStr string, isParent bool, mergeFn func(n *html.Node, nodes []*html.Node)) *Selection {
cache := make(map[string][]*html.Node)
for _, n := range s.Nodes {
var context *html.Node
if isParent {
context = n.Parent
} else {
if n.Type != html.ElementNode {
continue
}
context = n
}
if context == nil {
continue
}
nodes := cachedParseHtmlWithContext(cache, htmlStr, context)
mergeFn(n, cloneNodes(nodes))
}
return s
}
// cachedParseHtmlWithContext returns parseHtmlWithContext(htmlStr, context), reusing a prior
// result when context's nodeName has already been seen. Callers pass their own
// cache map so the cache lifetime matches the caller's loop scope.
func cachedParseHtmlWithContext(cache map[string][]*html.Node, htmlStr string, context *html.Node) []*html.Node {
key := nodeName(context)
if nodes, ok := cache[key]; ok {
return nodes
}
nodes := parseHtmlWithContext(htmlStr, context)
cache[key] = nodes
return nodes
}
+286
View File
@@ -1,6 +1,7 @@
package goquery
import (
"log"
"testing"
)
@@ -56,6 +57,26 @@ func TestAfterHtml(t *testing.T) {
printSel(t, doc.Selection)
}
func TestAfterHtmlContext(t *testing.T) {
doc := loadString(t, `
<html>
<body>
<table>
<tr>
<td>Before1</td>
</tr>
<tr>
<td>Before2</td>
</tr>
</table>
</body>
</html>`)
doc.Find("table tr td").AfterHtml("<td class='c1'>Test</td><td class='c2'>Again</td>")
assertLength(t, doc.Find("table tr td").Nodes, 6)
assertClass(t, doc.Find("table tr td").Last(), "c2")
printSel(t, doc.Selection)
}
func TestAppend(t *testing.T) {
doc := Doc2Clone()
doc.Find("#main").Append("#nf6")
@@ -113,6 +134,27 @@ func TestAppendHtml(t *testing.T) {
printSel(t, doc.Selection)
}
func TestAppendHtmlContext(t *testing.T) {
doc := loadString(t, `
<html>
<body>
<table>
<tr>
<td>Before1</td>
</tr>
<tr>
<td>Before2</td>
</tr>
</table>
</body>
</html>`)
doc.Find("table tr").AppendHtml("<td class='c1'>new1</td><td class='c2'>new2</td>")
assertLength(t, doc.Find("table td").Nodes, 6)
assertClass(t, doc.Find("table td").Last(), "c2")
printSel(t, doc.Selection)
}
func TestBefore(t *testing.T) {
doc := Doc2Clone()
doc.Find("#main").Before("#nf6")
@@ -151,6 +193,27 @@ func TestBeforeHtml(t *testing.T) {
printSel(t, doc.Selection)
}
func TestBeforeHtmlContext(t *testing.T) {
doc := loadString(t, `
<html>
<body>
<table>
<tr>
<td>Before1</td>
</tr>
<tr>
<td>Before2</td>
</tr>
</table>
</body>
</html>`)
doc.Find("table tr td:first-child").BeforeHtml("<td class='c1'>new1</td><td class='c2'>new2</td>")
assertLength(t, doc.Find("table td").Nodes, 6)
assertClass(t, doc.Find("table td").First(), "c1")
printSel(t, doc.Selection)
}
func TestEmpty(t *testing.T) {
doc := Doc2Clone()
s := doc.Find("#main").Empty()
@@ -218,6 +281,27 @@ func TestPrependHtml(t *testing.T) {
printSel(t, doc.Selection)
}
func TestPrependHtmlContext(t *testing.T) {
doc := loadString(t, `
<html>
<body>
<table>
<tr>
<td>Before1</td>
</tr>
<tr>
<td>Before2</td>
</tr>
</table>
</body>
</html>`)
doc.Find("table tr").PrependHtml("<td class='c1'>new node</td><td class='c2'>other new node</td>")
assertLength(t, doc.Find("table td").Nodes, 6)
assertClass(t, doc.Find("table tr td").First(), "c1")
printSel(t, doc.Selection)
}
func TestRemove(t *testing.T) {
doc := Doc2Clone()
doc.Find("#nf1").Remove()
@@ -278,6 +362,110 @@ func TestReplaceWithHtml(t *testing.T) {
printSel(t, doc.Selection)
}
func TestReplaceWithHtmlContext(t *testing.T) {
doc := loadString(t, `
<html>
<body>
<table>
<tr>
<th>Before1</th>
</tr>
<tr>
<th>Before2</th>
</tr>
</table>
</body>
</html>`)
doc.Find("table th").ReplaceWithHtml("<td class='c1'>Test</td><td class='c2'>Replace</td>")
assertLength(t, doc.Find("table th").Nodes, 0)
assertLength(t, doc.Find("table tr td").Nodes, 4)
assertClass(t, doc.Find("table tr td").First(), "c1")
printSel(t, doc.Selection)
}
func TestSetHtml(t *testing.T) {
doc := Doc2Clone()
q := doc.Find("#main, #foot")
q.SetHtml(`<div id="replace">test</div>`)
assertLength(t, doc.Find("#replace").Nodes, 2)
assertLength(t, doc.Find("#main, #foot").Nodes, 2)
if q.Text() != "testtest" {
t.Errorf("Expected text to be %v, found %v", "testtest", q.Text())
}
printSel(t, doc.Selection)
}
func TestSetHtmlNoMatch(t *testing.T) {
doc := Doc2Clone()
q := doc.Find("#notthere")
q.SetHtml(`<div id="replace">test</div>`)
assertLength(t, doc.Find("#replace").Nodes, 0)
printSel(t, doc.Selection)
}
func TestSetHtmlEmpty(t *testing.T) {
doc := Doc2Clone()
q := doc.Find("#main")
q.SetHtml(``)
assertLength(t, doc.Find("#main").Nodes, 1)
assertLength(t, doc.Find("#main").Children().Nodes, 0)
printSel(t, doc.Selection)
}
func TestSetHtmlContext(t *testing.T) {
doc := loadString(t, `
<html>
<body>
<table>
<tr>
<th>Before1</th>
</tr>
<tr>
<th>Before2</th>
</tr>
</table>
</body>
</html>`)
doc.Find("table tr").SetHtml("<td class='c1'>Test</td><td class='c2'>Again</td>")
assertLength(t, doc.Find("table th").Nodes, 0)
assertLength(t, doc.Find("table td").Nodes, 4)
assertLength(t, doc.Find("table tr").Nodes, 2)
printSel(t, doc.Selection)
}
func TestSetText(t *testing.T) {
doc := Doc2Clone()
q := doc.Find("#main, #foot")
repl := "<div id=\"replace\">test</div>"
q.SetText(repl)
assertLength(t, doc.Find("#replace").Nodes, 0)
assertLength(t, doc.Find("#main, #foot").Nodes, 2)
if q.Text() != (repl + repl) {
t.Errorf("Expected text to be %v, found %v", (repl + repl), q.Text())
}
h, err := q.Html()
if err != nil {
t.Errorf("Error: %v", err)
}
esc := "&lt;div id=&#34;replace&#34;&gt;test&lt;/div&gt;"
if h != esc {
t.Errorf("Expected html to be %v, found %v", esc, h)
}
printSel(t, doc.Selection)
}
func TestReplaceWithSelection(t *testing.T) {
doc := Doc2Clone()
sel := doc.Find("#nf6").ReplaceWithSelection(doc.Find("#nf5"))
@@ -451,3 +639,101 @@ func TestWrapInnerHtml(t *testing.T) {
printSel(t, doc.Selection)
}
func TestParsingRespectsVaryingContext(t *testing.T) {
docA := loadString(t, `
<html>
<body>
<a class="x"></a>
</body>
</html>`)
docTable := loadString(t, `
<html>
<body>
<table class="x"></table>
</body>
</html>`)
docBoth := loadString(t, `
<html>
<body>
<table class="x"></table>
<a class="x"></a>
</body>
</html>`)
sA := docA.Find(".x").AppendHtml("<tr><td>Hello</td></tr>")
sTable := docTable.Find(".x").AppendHtml("<tr><td>Hello</td></tr>")
sBoth := docBoth.Find(".x").AppendHtml("<tr><td>Hello</td></tr>")
printSel(t, docA.Selection)
printSel(t, docTable.Selection)
printSel(t, docBoth.Selection)
oA, _ := sA.Html()
oTable, _ := sTable.Html()
if oA == oTable {
t.Errorf("Expected inner html of <a> and <table> to not be equal, but got %s and %s", oA, oTable)
}
oBothTable, _ := sBoth.First().Html()
if oBothTable != oTable {
t.Errorf("Expected inner html of <table> and <table> in doc containing both tags to be equal, but got %s and %s",
oTable,
oBothTable)
}
oBothA, _ := sBoth.Last().Html()
if oBothA != oA {
t.Errorf("Expected inner html of <a> and <a> in doc containing both tags to be equal, but got %s and %s",
oA,
oBothA)
}
}
func TestHtmlWithNonElementNode(t *testing.T) {
const data = `
<html>
<head>
</head>
<body>
<p>
This is <span>some</span><b>text</b>.
</p>
</body>
</html>
`
cases := map[string]func(*Selection, string) *Selection{
"AfterHtml": (*Selection).AfterHtml,
"AppendHtml": (*Selection).AppendHtml,
"BeforeHtml": (*Selection).BeforeHtml,
"PrependHtml": (*Selection).PrependHtml,
"ReplaceWithHtml": (*Selection).ReplaceWithHtml,
"SetHtml": (*Selection).SetHtml,
}
for nm, fn := range cases {
// this test is only to make sure that the HTML parsing/manipulation
// methods do not raise panics when executed over Selections that contain
// non-Element nodes.
t.Run(nm, func(t *testing.T) {
doc := loadString(t, data)
sel := doc.Find("p").Contents()
func() {
defer func() {
if err := recover(); err != nil {
t.Fatal(err)
}
}()
fn(sel, "<div></div>")
}()
// print the resulting document in verbose mode
h, err := OuterHtml(doc.Selection)
if err != nil {
log.Fatal(err)
}
t.Log(h)
})
}
}
+27 -27
View File
@@ -1,37 +1,37 @@
#!/bin/sh
# Copyright 2012 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
# git gofmt pre-commit hook
#
# To use, store as .git/hooks/pre-commit inside your repository and make sure
# it has execute permissions.
#
# This script does not handle file names that contain spaces.
echo ">>> golint"
for dir in $(go list ./... | grep -v /vendor/)
do
golint "${dir}"
done
echo "<<< golint"
echo
# golint is purely informational, it doesn't fail with exit code != 0 if it finds something,
# because it may find a lot of false positives. Just print out its result for information.
echo "lint result (informational only):"
golint .
echo ">>> go vet"
go vet $(go list ./... | grep -v /vendor/)
echo "<<< go vet"
echo
# go vet returns 1 if an error was found. Exit the hook with this exit code.
go vet ./...
vetres=$?
echo ">>> gosimple"
gosimple $(go list ./... | grep -v /vendor/)
echo "<<< gosimple"
echo
# Check for gofmt problems and report if any.
gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$')
gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$' | grep -v /vendor/)
[ -z "$gofiles" ] && echo "EXIT $vetres" && exit $vetres
unformatted=$(gofmt -l $gofiles)
[ -z "$unformatted" ] && echo "EXIT $vetres" && exit $vetres
if [ -n "$gofiles" ]; then
unformatted=$(gofmt -l $gofiles)
# Some files are not gofmt'd. Print message and fail.
if [ -n "$unformatted" ]; then
# Some files are not gofmt'd.
echo >&2 "Go files must be formatted with gofmt. Please run:"
for fn in $unformatted; do
echo >&2 " gofmt -w $PWD/$fn"
done
fi
fi
echo
echo >&2 "Go files must be formatted with gofmt. Please run:"
for fn in $unformatted; do
echo >&2 " gofmt -w $PWD/$fn"
done
echo "EXIT 1"
exit 1
+51 -63
View File
@@ -1,37 +1,31 @@
package goquery
import (
"bytes"
"regexp"
"strings"
"golang.org/x/net/html"
)
var rxClassTrim = regexp.MustCompile("[\t\r\n]")
var classTrimReplacer = strings.NewReplacer("\t", " ", "\r", " ", "\n", " ")
// Attr gets the specified attribute's value for the first element in the
// Selection. To get the value for each element individually, use a looping
// construct such as Each or Map method.
func (s *Selection) Attr(attrName string) (val string, exists bool) {
if len(s.Nodes) == 0 {
return
func (s *Selection) Attr(attrName string) (string, bool) {
if len(s.Nodes) != 0 {
if attr := getAttributePtr(attrName, s.Nodes[0]); attr != nil {
return attr.Val, true
}
}
return getAttributeValue(attrName, s.Nodes[0])
return "", false
}
// AttrOr works like Attr but returns default value if attribute is not present.
func (s *Selection) AttrOr(attrName, defaultValue string) string {
if len(s.Nodes) == 0 {
return defaultValue
if val, exists := s.Attr(attrName); exists {
return val
}
val, exists := getAttributeValue(attrName, s.Nodes[0])
if !exists {
return defaultValue
}
return val
return defaultValue
}
// RemoveAttr removes the named attribute from each element in the set of matched elements.
@@ -60,13 +54,22 @@ func (s *Selection) SetAttr(attrName, val string) *Selection {
// Text gets the combined text contents of each element in the set of matched
// elements, including their descendants.
func (s *Selection) Text() string {
var buf bytes.Buffer
// Slightly optimized vs calling Each: no single selection object created
var builder strings.Builder
for _, n := range s.Nodes {
buf.WriteString(getNodeText(n))
s.textHelper(n, &builder)
}
return builder.String()
}
func (s *Selection) textHelper(n *html.Node, builder *strings.Builder) {
if n.Type == html.TextNode {
// Keep newlines and spaces, like jQuery
builder.WriteString(n.Data)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
s.textHelper(c, builder)
}
return buf.String()
}
// Size is an alias for Length.
@@ -84,16 +87,16 @@ func (s *Selection) Length() int {
func (s *Selection) Html() (ret string, e error) {
// Since there is no .innerHtml, the HTML content must be re-created from
// the nodes using html.Render.
var buf bytes.Buffer
var builder strings.Builder
if len(s.Nodes) > 0 {
for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
e = html.Render(&buf, c)
e = html.Render(&builder, c)
if e != nil {
return
}
}
ret = buf.String()
ret = builder.String()
}
return
@@ -110,9 +113,9 @@ func (s *Selection) AddClass(class ...string) *Selection {
tcls := getClassesSlice(classStr)
for _, n := range s.Nodes {
curClasses, attr := getClassesAndAttr(n, true)
curClasses, attr := getClassesAndAttr(n)
for _, newClass := range tcls {
if strings.Index(curClasses, " "+newClass+" ") == -1 {
if !strings.Contains(curClasses, " "+newClass+" ") {
curClasses += newClass + " "
}
}
@@ -126,11 +129,20 @@ func (s *Selection) AddClass(class ...string) *Selection {
// HasClass determines whether any of the matched elements are assigned the
// given class.
func (s *Selection) HasClass(class string) bool {
rawClass := class
class = " " + class + " "
for _, n := range s.Nodes {
classes, _ := getClassesAndAttr(n, false)
if strings.Index(classes, class) > -1 {
return true
if n.Type != html.ElementNode {
continue
}
if attr := getAttributePtr("class", n); attr != nil {
if !strings.Contains(attr.Val, rawClass) {
continue
}
val := classTrimReplacer.Replace(attr.Val)
if strings.Contains(" "+val+" ", class) {
return true
}
}
}
return false
@@ -153,9 +165,9 @@ func (s *Selection) RemoveClass(class ...string) *Selection {
if remove {
removeAttr(n, "class")
} else {
classes, attr := getClassesAndAttr(n, true)
classes, attr := getClassesAndAttr(n)
for _, rcl := range rclasses {
classes = strings.Replace(classes, " "+rcl+" ", " ", -1)
classes = strings.ReplaceAll(classes, " "+rcl+" ", " ")
}
setClasses(n, attr, classes)
@@ -177,10 +189,11 @@ func (s *Selection) ToggleClass(class ...string) *Selection {
tcls := getClassesSlice(classStr)
for _, n := range s.Nodes {
classes, attr := getClassesAndAttr(n, true)
classes, attr := getClassesAndAttr(n)
for _, tcl := range tcls {
if strings.Index(classes, " "+tcl+" ") != -1 {
classes = strings.Replace(classes, " "+tcl+" ", " ", -1)
spaceAroundTcl := " " + tcl + " "
if strings.Contains(classes, spaceAroundTcl) {
classes = strings.ReplaceAll(classes, spaceAroundTcl, " ")
} else {
classes += tcl + " "
}
@@ -192,22 +205,6 @@ func (s *Selection) ToggleClass(class ...string) *Selection {
return s
}
// Get the specified node's text content.
func getNodeText(node *html.Node) string {
if node.Type == html.TextNode {
// Keep newlines and spaces, like jQuery
return node.Data
} else if node.FirstChild != nil {
var buf bytes.Buffer
for c := node.FirstChild; c != nil; c = c.NextSibling {
buf.WriteString(getNodeText(c))
}
return buf.String()
}
return ""
}
func getAttributePtr(attrName string, n *html.Node) *html.Attribute {
if n == nil {
return nil
@@ -221,21 +218,12 @@ func getAttributePtr(attrName string, n *html.Node) *html.Attribute {
return nil
}
// Private function to get the specified attribute's value from a node.
func getAttributeValue(attrName string, n *html.Node) (val string, exists bool) {
if a := getAttributePtr(attrName, n); a != nil {
val = a.Val
exists = true
}
return
}
// Get and normalize the "class" attribute from the node.
func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.Attribute) {
func getClassesAndAttr(n *html.Node) (classes string, attr *html.Attribute) {
// Applies only to element nodes
if n.Type == html.ElementNode {
attr = getAttributePtr("class", n)
if attr == nil && create {
if attr == nil {
n.Attr = append(n.Attr, html.Attribute{
Key: "class",
Val: "",
@@ -247,14 +235,14 @@ func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.At
if attr == nil {
classes = " "
} else {
classes = rxClassTrim.ReplaceAllString(" "+attr.Val+" ", " ")
classes = classTrimReplacer.Replace(" " + attr.Val + " ")
}
return
}
func getClassesSlice(classes string) []string {
return strings.Split(rxClassTrim.ReplaceAllString(" "+classes+" ", " "), " ")
return strings.Fields(classes)
}
func removeAttr(n *html.Node, attrName string) {
+21 -17
View File
@@ -1,49 +1,53 @@
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 false
return s.IsMatcher(compileMatcher(selector))
}
// IsMatcher checks the current matched set of elements against a matcher and
// returns true if at least one of these elements matches.
func (s *Selection) IsMatcher(m Matcher) bool {
if len(s.Nodes) > 0 {
if len(s.Nodes) == 1 {
return m.Match(s.Nodes[0])
for _, n := range s.Nodes {
if m.Match(n) {
return true
}
return len(m.Filter(s.Nodes)) > 0
}
return false
}
// IsFunction checks the current matched set of elements against a predicate and
// returns true if at least one of these elements matches.
func (s *Selection) IsFunction(f func(int, *Selection) bool) bool {
return s.FilterFunction(f).Length() > 0
for i, n := range s.Nodes {
if f(i, newSingleSelection(n, s.document)) {
return true
}
}
return false
}
// IsSelection checks the current matched set of elements against a Selection object
// and returns true if at least one of these elements matches.
func (s *Selection) IsSelection(sel *Selection) bool {
return s.FilterSelection(sel).Length() > 0
if sel == nil {
return false
}
return s.IsNodes(sel.Nodes...)
}
// IsNodes checks the current matched set of elements against the specified nodes
// and returns true if at least one of these elements matches.
func (s *Selection) IsNodes(nodes ...*html.Node) bool {
return s.FilterNodes(nodes...).Length() > 0
for _, n := range s.Nodes {
if isInSlice(nodes, n) {
return true
}
}
return false
}
// Contains returns true if the specified Node is within,
+7
View File
@@ -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)") {
+77 -47
View File
@@ -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
@@ -23,8 +20,14 @@ const (
// Find gets the descendants of each element in the current set of matched
// elements, filtered by a selector. It returns a new Selection object
// containing these matched elements.
//
// Note that as for all methods accepting a selector string, the selector is
// compiled and applied by the cascadia package and inherits its behavior and
// constraints regarding supported selectors. See the note on cascadia in
// the goquery documentation here:
// https://github.com/PuerkitoBio/goquery?tab=readme-ov-file#api
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 +96,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 +115,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 +127,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)
}
@@ -146,11 +149,15 @@ func (s *Selection) ClosestMatcher(m Matcher) *Selection {
// ClosestNodes gets the first element that matches one of the nodes by testing the
// element itself and traversing up through its ancestors in the DOM tree.
func (s *Selection) ClosestNodes(nodes ...*html.Node) *Selection {
set := make(map[*html.Node]bool, len(nodes))
for _, n := range nodes {
set[n] = true
}
return pushStack(s, mapNodes(s.Nodes, func(i int, n *html.Node) []*html.Node {
// For each node in the selection, test the node itself, then each parent
// until a match is found.
for ; n != nil; n = n.Parent {
if isInSlice(nodes, n) {
if set[n] {
return []*html.Node{n}
}
}
@@ -177,7 +184,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 +197,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 +228,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 +241,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 +258,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 +278,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 +298,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 +318,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 +338,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 +358,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 +373,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 +407,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 +441,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 +456,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 +474,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 +490,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 +505,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 +523,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
@@ -530,10 +537,7 @@ func (s *Selection) PrevMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *
// Filter and push filters the nodes based on a matcher, and pushes the results
// on the stack, with the srcSel as previous selection.
func filterAndPush(srcSel *Selection, nodes []*html.Node, m Matcher) *Selection {
// Create a temporary Selection with the specified nodes to filter using winnow
sel := &Selection{nodes, srcSel.document, nil}
// Filter based on matcher and push on stack
return pushStack(srcSel, winnow(sel, m, true))
return pushStack(srcSel, winnow(nodes, m, true))
}
// Internal implementation of Find that return raw nodes.
@@ -555,13 +559,12 @@ func findWithMatcher(nodes []*html.Node, m Matcher) []*html.Node {
func getParentsNodes(nodes []*html.Node, stopm Matcher, stopNodes []*html.Node) []*html.Node {
return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
for p := n.Parent; p != nil; p = p.Parent {
sel := newSingleSelection(p, nil)
if stopm != nil {
if sel.IsMatcher(stopm) {
if stopm.Match(p) {
break
}
} else if len(stopNodes) > 0 {
if sel.IsNodes(stopNodes...) {
if isInSlice(stopNodes, p) {
break
}
}
@@ -582,13 +585,9 @@ func getSiblingNodes(nodes []*html.Node, st siblingType, untilm Matcher, untilNo
if st == siblingNextUntil || st == siblingPrevUntil {
f = func(n *html.Node) bool {
if untilm != nil {
// Matcher-based condition
sel := newSingleSelection(n, nil)
return sel.IsMatcher(untilm)
return untilm.Match(n)
} else if len(untilNodes) > 0 {
// Nodes-based condition
sel := newSingleSelection(n, nil)
return sel.IsNodes(untilNodes...)
return isInSlice(untilNodes, n)
}
return false
}
@@ -655,6 +654,20 @@ func getChildrenWithSiblingType(parent *html.Node, st siblingType, skipNode *htm
}
}
// For the cases that collect every matching sibling, count them in a
// cheap pointer walk first so the result slice can be sized exactly,
// avoiding repeated slice growth. The Until cases are skipped (counting
// would require running the predicate twice) and so are the single-result
// Next/Prev cases.
switch st {
case siblingAll, siblingAllIncludingNonElements, siblingPrevAll, siblingNextAll:
n := 0
for c := iter(nil); c != nil; c = iter(c) {
n++
}
result = make([]*html.Node, 0, n)
}
for c := iter(nil); c != nil; c = iter(c) {
// If this is an ...Until case, test before append (returns true
// if the until condition is reached)
@@ -674,12 +687,21 @@ func getChildrenWithSiblingType(parent *html.Node, st siblingType, skipNode *htm
// Internal implementation of parent nodes that return a raw slice of Nodes.
func getParentNodes(nodes []*html.Node) []*html.Node {
return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
if n.Parent != nil && n.Parent.Type == html.ElementNode {
return []*html.Node{n.Parent}
// Collect parents inline rather than going through mapNodes, which would
// allocate a throwaway one-element slice per source node. Many source
// nodes (e.g. siblings) share the same parent, so deduplicate as we go.
var result []*html.Node
set := make(map[*html.Node]bool, len(nodes))
for _, n := range nodes {
p := n.Parent
if p == nil || p.Type != html.ElementNode || set[p] {
continue
}
return nil
})
set[p] = true
result = append(result, p)
}
return result
}
// Internal map function used by many traversing methods. Takes the source nodes
@@ -687,10 +709,18 @@ func getParentNodes(nodes []*html.Node) []*html.Node {
// Returns an array of nodes mapped by calling the callback function once for
// each node in the source nodes.
func mapNodes(nodes []*html.Node, f func(int, *html.Node) []*html.Node) (result []*html.Node) {
switch len(nodes) {
case 0:
return nil
case 1:
return f(0, nodes[0])
}
set := make(map[*html.Node]bool, len(nodes))
for i, n := range nodes {
if vals := f(i, n); len(vals) > 0 {
result = appendWithoutDuplicates(result, vals)
result = appendWithoutDuplicates(result, vals, set)
}
}
return
return result
}
+116 -3
View File
@@ -3,6 +3,8 @@ package goquery
import (
"strings"
"testing"
"golang.org/x/net/html"
)
func TestFind(t *testing.T) {
@@ -21,9 +23,19 @@ 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 TestFindBig(t *testing.T) {
doc := DocW()
sel := doc.Find("li")
assertLength(t, sel.Nodes, 373)
sel2 := doc.Find("span")
assertLength(t, sel2.Nodes, 448)
sel3 := sel.FindSelection(sel2)
assertLength(t, sel3.Nodes, 248)
}
func TestChainedFind(t *testing.T) {
@@ -31,11 +43,31 @@ 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)
}
func TestChildrenDuplicateSourceNodes(t *testing.T) {
// A Selection's Nodes field is exported, so callers can construct a
// selection that contains the same node more than once. Children (and the
// other children-based traversals) must still deduplicate so that the
// shared child set is not returned multiple times.
base := Doc().Find(".pvk-content")
node := base.Nodes[0]
expected := base.Eq(0).Children().Length()
dup := &Selection{Nodes: []*html.Node{node, node}, document: base.document}
assertLength(t, dup.Children().Nodes, expected)
assertLength(t, dup.ChildrenFiltered("*").Nodes, expected)
assertLength(t, dup.Contents().Nodes, base.Eq(0).Contents().Length())
}
func TestChildrenRollback(t *testing.T) {
sel := Doc().Find(".pvk-content")
sel2 := sel.Children().End()
@@ -58,6 +90,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 +106,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 +144,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 +177,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 +193,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 +237,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 +302,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 +339,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 +382,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 +419,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 +462,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 +479,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 +538,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 +597,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 +644,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 +705,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")
+100 -3
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
@@ -29,6 +30,10 @@ func NewDocumentFromNode(root *html.Node) *Document {
// NewDocument is a Document constructor that takes a string URL as argument.
// It loads the specified document, parses it, and stores the root Document
// node, ready to be manipulated.
//
// Deprecated: Use the net/http standard library package to make the request
// and validate the response before calling goquery.NewDocumentFromReader
// with the response's body.
func NewDocument(url string) (*Document, error) {
// Load the URL
res, e := http.Get(url)
@@ -38,10 +43,10 @@ func NewDocument(url string) (*Document, error) {
return NewDocumentFromResponse(res)
}
// NewDocumentFromReader returns a Document from a generic reader.
// NewDocumentFromReader returns a Document from an io.Reader.
// It returns an error as second value if the reader's data cannot be parsed
// as html. It does *not* check if the reader is also an io.Closer, so the
// provided reader is never closed by this call, it is the responsibility
// as html. It does not check if the reader is also an io.Closer, the
// provided reader is never closed by this call. It is the responsibility
// of the caller to close it if required.
func NewDocumentFromReader(r io.Reader) (*Document, error) {
root, e := html.Parse(r)
@@ -54,6 +59,8 @@ func NewDocumentFromReader(r io.Reader) (*Document, error) {
// NewDocumentFromResponse is another Document constructor that takes an http response as argument.
// It loads the specified response's document, parses it, and stores the root Document
// node, ready to be manipulated. The response's body is closed on return.
//
// Deprecated: Use goquery.NewDocumentFromReader with the response's body.
func NewDocumentFromResponse(res *http.Response) (*Document, error) {
if res == nil {
return nil, errors.New("Response is nil")
@@ -113,3 +120,93 @@ type Matcher interface {
MatchAll(*html.Node) []*html.Node
Filter([]*html.Node) []*html.Node
}
// Single compiles a selector string to a Matcher that stops after the first
// match is found.
//
// By default, Selection.Find and other functions that accept a selector string
// to select nodes will use all matches corresponding to that selector. By
// using the Matcher returned by Single, at most the first match will be
// selected.
//
// For example, those two statements are semantically equivalent:
//
// sel1 := doc.Find("a").First()
// sel2 := doc.FindMatcher(goquery.Single("a"))
//
// The one using Single is optimized to be potentially much faster on large
// documents.
//
// Only the behaviour of the MatchAll method of the Matcher interface is
// altered compared to standard Matchers. This means that the single-selection
// property of the Matcher only applies for Selection methods where the Matcher
// is used to select nodes, not to filter or check if a node matches the
// Matcher - in those cases, the behaviour of the Matcher is unchanged (e.g.
// FilterMatcher(Single("div")) will still result in a Selection with multiple
// "div"s if there were many "div"s in the Selection to begin with).
func Single(selector string) Matcher {
return singleMatcher{compileMatcher(selector)}
}
// SingleMatcher returns a Matcher matches the same nodes as m, but that stops
// after the first match is found.
//
// See the documentation of function Single for more details.
func SingleMatcher(m Matcher) Matcher {
if _, ok := m.(singleMatcher); ok {
// m is already a singleMatcher
return m
}
return singleMatcher{m}
}
// 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
}
type singleMatcher struct {
Matcher
}
// MatchFirst returns the first node that matches, or nil if none does. It uses
// the underlying Matcher's MatchFirst method if it provides one (cascadia-
// compiled matchers all do), avoiding building a result slice. Otherwise it
// falls back to MatchAll and returns its first element.
func (m singleMatcher) MatchFirst(n *html.Node) *html.Node {
// Optimized version - stops finding at the first match (cascadia-compiled
// matchers all use this code path).
if mm, ok := m.Matcher.(interface{ MatchFirst(*html.Node) *html.Node }); ok {
return mm.MatchFirst(n)
}
// Fallback version, for e.g. test mocks that don't provide the MatchFirst
// method.
nodes := m.Matcher.MatchAll(n)
if len(nodes) > 0 {
return nodes[0]
}
return nil
}
func (m singleMatcher) MatchAll(n *html.Node) []*html.Node {
// Wrap MatchFirst's single result in a slice to respect the Matcher
// interface's MatchAll signature.
if node := m.MatchFirst(n); node != nil {
return []*html.Node{node}
}
return nil
}
// 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 }
+72 -9
View File
@@ -4,8 +4,10 @@ import (
"bytes"
"fmt"
"os"
"strings"
"testing"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
@@ -22,45 +24,42 @@ func Doc() *Document {
}
return doc
}
func DocClone() *Document {
return CloneDocument(Doc())
}
func Doc2() *Document {
if doc2 == nil {
doc2 = loadDoc("page2.html")
}
return doc2
}
func Doc2Clone() *Document {
return CloneDocument(Doc2())
}
func Doc3() *Document {
if doc3 == nil {
doc3 = loadDoc("page3.html")
}
return doc3
}
func Doc3Clone() *Document {
return CloneDocument(Doc3())
}
func DocB() *Document {
if docB == nil {
docB = loadDoc("gotesting.html")
}
return docB
}
func DocBClone() *Document {
return CloneDocument(DocB())
}
func DocW() *Document {
if docW == nil {
docW = loadDoc("gowiki.html")
}
return docW
}
func DocWClone() *Document {
return CloneDocument(DocW())
}
func assertLength(t *testing.T, nodes []*html.Node, length int) {
if len(nodes) != length {
@@ -123,6 +122,14 @@ func loadDoc(page string) *Document {
return NewDocumentFromNode(node)
}
func loadString(t *testing.T, doc string) *Document {
d, err := NewDocumentFromReader(strings.NewReader(doc))
if err != nil {
t.Error("Failed to parse test document")
}
return d
}
func TestNewDocument(t *testing.T) {
if f, e := os.Open("./testdata/page.html"); e != nil {
t.Error(e.Error())
@@ -190,3 +197,59 @@ func TestNewDocumentFromResponseNil(t *testing.T) {
t.Error("Expected error, got none")
}
}
func TestIssue103(t *testing.T) {
d, err := NewDocumentFromReader(strings.NewReader("<html><title>Scientists Stored These Images in DNA—Then Flawlessly Retrieved Them</title></html>"))
if err != nil {
t.Error(err)
}
text := d.Find("title").Text()
for i, r := range text {
t.Logf("%d: %d - %q\n", i, r, string(r))
}
t.Log(text)
}
func TestSingle(t *testing.T) {
data := `
<html>
<body>
<div class="b">1</div>
<div class="a">2</div>
<div class="a">3</div>
<p class="b">4</p>
</body>
</html>
`
doc, err := NewDocumentFromReader(strings.NewReader(data))
if err != nil {
t.Fatal(err)
}
text := doc.FindMatcher(Single("div")).Text()
if text != "1" {
t.Fatalf("want %q, got %q", "1", text)
}
// Verify semantic equivalence
sel1 := doc.Find("div").First()
sel2 := doc.FindMatcher(Single("div"))
if sel1.Text() != sel2.Text() {
t.Fatalf("want sel1 to equal sel2")
}
// Here, the Single has no effect as the selector is used to filter
// from the existing selection, not to find nodes in the document.
divs := doc.Find("div")
text = divs.FilterMatcher(Single(".a")).Text()
if text != "23" {
t.Fatalf("want %q, got %q", "23", text)
}
classA := cascadia.MustCompile(".a")
classB := cascadia.MustCompile(".b")
text = doc.FindMatcher(classB).AddMatcher(SingleMatcher(classA)).Text()
if text != "142" {
t.Fatalf("want %q, got %q", "142", text)
}
}
+132 -31
View File
@@ -1,11 +1,17 @@
package goquery
import (
"bytes"
"io"
"strings"
"golang.org/x/net/html"
)
// used to determine if a set (map[*html.Node]bool) should be used
// instead of iterating over a slice. The set uses more memory and
// is slower than slice iteration for small N.
const minNodesForSet = 1000
var nodeNames = []string{
html.ErrorNode: "#error",
html.TextNode: "#text",
@@ -20,53 +26,127 @@ var nodeNames = []string{
// Go's net/html package defines the following node types, listed with
// the corresponding returned value from this function:
//
// ErrorNode : #error
// TextNode : #text
// DocumentNode : #document
// ElementNode : the element's tag name
// CommentNode : #comment
// DoctypeNode : the name of the document type
//
// ErrorNode : #error
// TextNode : #text
// DocumentNode : #document
// ElementNode : the element's tag name
// CommentNode : #comment
// DoctypeNode : the name of the document type
func NodeName(s *Selection) string {
if s.Length() == 0 {
return ""
}
switch n := s.Get(0); n.Type {
return nodeName(s.Get(0))
}
// nodeName returns the node name of the given html node.
// See NodeName for additional details on behaviour.
func nodeName(node *html.Node) string {
if node == nil {
return ""
}
switch node.Type {
case html.ElementNode, html.DoctypeNode:
return n.Data
return node.Data
default:
if n.Type >= 0 && int(n.Type) < len(nodeNames) {
return nodeNames[n.Type]
if int(node.Type) < len(nodeNames) {
return nodeNames[node.Type]
}
return ""
}
}
// TextOptions controls the behaviour of the Text function.
type TextOptions struct {
// Separator is inserted between the contents of the text nodes that are
// included in the result. It defaults to the empty string, which
// concatenates the text nodes without any separator.
Separator string
// Trim, when true, removes leading and trailing whitespace from the
// content of each text node and omits text nodes that are empty once
// trimmed. This is useful to discard the insignificant whitespace that
// comes from the indentation of the source HTML.
Trim bool
// Keep, when non-nil, is called for each text node encountered while
// traversing the selection. The content of the text node is included in
// the result only if Keep returns true. It can be used, for example, to
// drop the text of <script> and <style> elements by inspecting the node's
// parent. When Keep is nil, every text node is included (subject to Trim).
Keep func(node *html.Node) bool
}
// Text returns the combined text contents of the nodes in the selection,
// including their descendants, in document order. It is a package-level
// function - and not a method on the Selection, because it is not part of the
// jQuery API - that offers control over how the text of distinct text nodes is
// joined and which text nodes are included, in the same spirit as Python's
// BeautifulSoup get_text.
//
// With a nil opts (or a zero-value TextOptions), Text behaves like calling the
// Selection.Text method on the selection. Setting TextOptions.Separator inserts
// a separator between the text nodes, Trim strips the surrounding whitespace of
// each text node (dropping the ones that become empty), and Keep filters which
// text nodes contribute to the result.
func Text(s *Selection, opts *TextOptions) string {
if opts == nil {
opts = &TextOptions{}
}
var parts []string
var collect func(*html.Node)
collect = func(n *html.Node) {
if n.Type == html.TextNode {
if opts.Keep != nil && !opts.Keep(n) {
return
}
text := n.Data
if opts.Trim {
text = strings.TrimSpace(text)
if text == "" {
return
}
}
parts = append(parts, text)
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
collect(c)
}
}
for _, n := range s.Nodes {
collect(n)
}
return strings.Join(parts, opts.Separator)
}
// Render renders the HTML of the first item in the selection and writes it to
// the writer. It behaves the same as OuterHtml but writes to w instead of
// returning the string.
func Render(w io.Writer, s *Selection) error {
if s.Length() == 0 {
return nil
}
n := s.Get(0)
return html.Render(w, n)
}
// OuterHtml returns the outer HTML rendering of the first item in
// the selection - that is, the HTML including the first element's
// tag and attributes.
//
// Unlike InnerHtml, this is a function and not a method on the Selection,
// Unlike Html, this is a function and not a method on the Selection,
// because this is not a jQuery method (in javascript-land, this is
// a property provided by the DOM).
func OuterHtml(s *Selection) (string, error) {
var buf bytes.Buffer
if s.Length() == 0 {
return "", nil
}
n := s.Get(0)
if err := html.Render(&buf, n); err != nil {
var builder strings.Builder
if err := Render(&builder, s); err != nil {
return "", err
}
return buf.String(), nil
}
func getChildren(n *html.Node) (result []*html.Node) {
for c := n.FirstChild; c != nil; c = c.NextSibling {
result = append(result, c)
}
return result
return builder.String(), nil
}
// Loop through all container nodes to search for the target node.
@@ -112,11 +192,32 @@ func indexInSlice(slice []*html.Node, node *html.Node) int {
// Appends the new nodes to the target slice, making sure no duplicate is added.
// There is no check to the original state of the target slice, so it may still
// contain duplicates. The target slice is returned because append() may create
// a new underlying array.
func appendWithoutDuplicates(target []*html.Node, nodes []*html.Node) []*html.Node {
// a new underlying array. If targetSet is nil, a local set is created with the
// target if len(target) + len(nodes) is greater than minNodesForSet.
func appendWithoutDuplicates(target []*html.Node, nodes []*html.Node, targetSet map[*html.Node]bool) []*html.Node {
// if there are not that many nodes, don't use the map, faster to just use nested loops
// (unless a non-nil targetSet is passed, in which case the caller knows better).
if targetSet == nil && len(target)+len(nodes) < minNodesForSet {
for _, n := range nodes {
if !isInSlice(target, n) {
target = append(target, n)
}
}
return target
}
// if a targetSet is passed, then assume it is reliable, otherwise create one
// and initialize it with the current target contents.
if targetSet == nil {
targetSet = make(map[*html.Node]bool, len(target)+len(nodes))
for _, n := range target {
targetSet[n] = true
}
}
for _, n := range nodes {
if !isInSlice(target, n) {
if !targetSet[n] {
target = append(target, n)
targetSet[n] = true
}
}
+107 -1
View File
@@ -77,7 +77,113 @@ func TestNodeNameMultiSel(t *testing.T) {
sort.Strings(in)
sort.Strings(out)
if !reflect.DeepEqual(in, out) {
t.Error("want %v, got %v")
t.Errorf("want %v, got %v", in, out)
}
}
var textNodes = `<!doctype html>
<html>
<body>
<div id="content">
<h1> Hello </h1>
<p>world</p>
<!-- a comment -->
<script>var ignored = 1;</script>
</div>
</body>
</html>`
func TestText_NilOptions(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader(textNodes))
if err != nil {
t.Fatal(err)
}
sel := doc.Find("#content")
// A nil options value must behave exactly like the Text method.
if got, want := Text(sel, nil), sel.Text(); got != want {
t.Errorf("nil options: want %q, got %q", want, got)
}
}
func TestText_SeparatorAndTrim(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader(textNodes))
if err != nil {
t.Fatal(err)
}
// Trim drops the whitespace-only text nodes coming from indentation, and
// Separator joins the remaining fragments. The <script> text is included
// because no Keep filter is provided.
got := Text(doc.Find("#content"), &TextOptions{Separator: "|", Trim: true})
want := "Hello|world|var ignored = 1;"
if got != want {
t.Errorf("want %q, got %q", want, got)
}
}
func TestText_Keep(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader(textNodes))
if err != nil {
t.Fatal(err)
}
// Keep skips the text of <script> (and <style>) elements.
keep := func(n *html.Node) bool {
if n.Parent != nil && n.Parent.Type == html.ElementNode {
switch n.Parent.Data {
case "script", "style":
return false
}
}
return true
}
got := Text(doc.Find("#content"), &TextOptions{Separator: " ", Trim: true, Keep: keep})
want := "Hello world"
if got != want {
t.Errorf("want %q, got %q", want, got)
}
}
func TestText_MultiSelection(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader(textNodes))
if err != nil {
t.Fatal(err)
}
got := Text(doc.Find("h1, p"), &TextOptions{Separator: ",", Trim: true})
want := "Hello,world"
if got != want {
t.Errorf("want %q, got %q", want, got)
}
}
func TestText_EmptySelection(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader(textNodes))
if err != nil {
t.Fatal(err)
}
// An empty selection has no text nodes to walk, so the result is empty
// and the call must not panic.
got := Text(doc.Find(".no-such-thing"), &TextOptions{Separator: " ", Trim: true})
if got != "" {
t.Errorf("want empty string, got %q", got)
}
}
func TestText_KeepFiltersAll(t *testing.T) {
doc, err := NewDocumentFromReader(strings.NewReader(textNodes))
if err != nil {
t.Fatal(err)
}
// When Keep rejects every text node, nothing is joined and the result is
// empty rather than a string of separators.
keep := func(n *html.Node) bool { return false }
got := Text(doc.Find("#content"), &TextOptions{Separator: " ", Trim: true, Keep: keep})
if got != "" {
t.Errorf("want empty string, got %q", got)
}
}