Repository: mmcdole/gofeed

Issue:
v2: Modernize dependencies and prepare v2 module
## Overview
Part of gofeed v2 development. This issue tracks modernizing dependencies, updating the Go version, and preparing the module for v2 release following Go module conventions.

## Goals
- Update Go version to a more recent stable version (e.g., Go 1.21 or 1.22)
- Review and update all dependencies to their latest stable versions
- Replace dependencies with stdlib alternatives where possible
- Ensure minimal dependency footprint for the core library
- Properly version the module as v2 following Go module conventions

## Tasks

### Dependencies & Modernization
- [ ] Update Go version in go.mod (1.21+)
- [ ] Audit all current dependencies for:
  - Security vulnerabilities
  - Maintenance status (last updated, open issues)
  - Whether they can be replaced with stdlib
- [ ] Update all dependencies to latest stable versions
- [ ] Run `go mod tidy` to clean up
- [ ] Document any dependencies that could be replaced with stdlib but require significant refactoring

### v2 Module Preparation
- [ ] Change module declaration to `module github.com/mmcdole/gofeed/v2`
- [ ] Update all internal imports to use /v2 suffix
- [ ] Create migration guide for v1 to v2 users
- [ ] Document breaking changes and minimum Go version requirements

### Future Considerations
- [ ] Evaluate moving `cmd/ftest` to a separate module to avoid pulling in CLI dependencies (currently uses urfave/cli) - see #128
- [ ] Consider if any other structural changes would benefit library users

## Current Dependencies Review
The library currently depends on:
- `PuerkitoBio/goquery` - HTML parsing for feed detection
- `json-iterator/go` - JSON parsing performance
- `mmcdole/goxpp` - XML pull parsing
- `urfave/cli` - Only used by cmd/ftest, not the library itself
- `golang.org/x/net` - HTML parsing support
- `golang.org/x/text` - Character encoding

## Version Migration Strategy
Following Go module conventions for v2:
- Module path becomes `github.com/mmcdole/gofeed/v2`
- Users import with: `import "github.com/mmcdole/gofeed/v2"`
- v1 continues to work unchanged at `github.com/mmcdole/gofeed`
- This prevents breaking existing v1 users when v2 is released

## References
- Related to v2 RFC: #241
- Issue #128 suggested making cmd/ftest a separate module
- Go modules v2 documentation: https://go.dev/blog/v2-go-modules

## Labels
- v2
- enhancement

Discussion:
@mmcdole: This should be completed.

Solution patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 9a74bb9..ef9b217 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -19,7 +19,7 @@ jobs:
     - name: Set up Go
       uses: actions/setup-go@v4
       with:
-        go-version: '1.20'
+        go-version: '1.21'
 
     - name: Build
       run: go build -v ./...
diff --git a/README.md b/README.md
index a154b16..43413e7 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@
 
 ## Table of Contents
 - [Features](#features)
+- [Installation](#installation)
 - [Overview](#overview)
 - [Basic Usage](#basic-usage)
 - [Advanced Usage](#advanced-usage)
@@ -42,6 +43,28 @@ For added convenience, gofeed includes native support for parsing certain well-k
 
 - Dublin Core: Accessible via `Feed.DublinCoreExt` and `Item.DublinCoreExt`
 - Apple iTunes: Accessible via `Feed.ITunesExt` and `Item.ITunesExt`
+
+## Installation
+
+```bash
+go get github.com/mmcdole/gofeed/v2
+```
+
+Import in your code:
+
+```go
+import "github.com/mmcdole/gofeed/v2"
+```
+
+For feed-specific parsers:
+
+```go
+import (
+    "github.com/mmcdole/gofeed/v2/rss"
+    "github.com/mmcdole/gofeed/v2/atom" 
+    "github.com/mmcdole/gofeed/v2/json"
+)
+```
   
 ## Overview
 
@@ -66,6 +89,11 @@ Here's how to parse feeds using `gofeed.Parser`:
 
 #### From a URL
 ```go
+import (
+    "fmt"
+    "github.com/mmcdole/gofeed/v2"
+)
+
 fp := gofeed.NewParser()
 feed, _ := fp.ParseURL("http://feeds.twit.tv/twit.xml")
 fmt.Println(feed.Title)
@@ -120,6 +148,11 @@ If you have a usage scenario that requires a specialized parser:
 #### RSS Feed
 
 ```go
+import (
+    "strings"
+    "github.com/mmcdole/gofeed/v2/rss"
+)
+
 feedData := `<rss version="2.0">
 <channel>
 <webMaster>example@site.com (Example Name)</webMaster>
diff --git a/atom/feed.go b/atom/feed.go
index 34f6afc..cd24dd3 100644
--- a/atom/feed.go
+++ b/atom/feed.go
@@ -4,7 +4,7 @@ import (
 	"encoding/json"
 	"time"
 
-	"github.com/mmcdole/gofeed/extensions"
+	"github.com/mmcdole/gofeed/v2/extensions"
 )
 
 // Feed is an Atom Feed
diff --git a/atom/parser.go b/atom/parser.go
index 04ed0af..903a6de 100644
--- a/atom/parser.go
+++ b/atom/parser.go
@@ -5,9 +5,8 @@ import (
 	"io"
 	"strings"
 
-	"github.com/PuerkitoBio/goquery"
-	ext "github.com/mmcdole/gofeed/extensions"
-	"github.com/mmcdole/gofeed/internal/shared"
+	ext "github.com/mmcdole/gofeed/v2/extensions"
+	"github.com/mmcdole/gofeed/v2/internal/shared"
 	xpp "github.com/mmcdole/goxpp"
 )
 
@@ -786,18 +785,6 @@ func (ap *Parser) parseVersion(p *xpp.XMLPullParser) string {
 	return ""
 }
 
-func (ap *Parser) stripWrappingDiv(content string) (result string) {
-	result = content
-	r := strings.NewReader(result)
-	doc, err := goquery.NewDocumentFromReader(r)
-	if err == nil {
-		root := doc.Find("body").Children()
-		if root.Is("div") && root.Siblings().Size() == 0 {
-			html, err := root.Unwrap().Html()
-			if err == nil {
-				result = html
-			}
-		}
-	}
-	return
+func (ap *Parser) stripWrappingDiv(content string) string {
+	return shared.StripWrappingDiv(content)
 }
diff --git a/detector.go b/detector.go
index 4daf408..d61c9ff 100644
--- a/detector.go
+++ b/detector.go
@@ -2,11 +2,11 @@ package gofeed
 
 import (
 	"bytes"
+	"encoding/json"
 	"io"
 	"strings"
 
-	jsoniter "github.com/json-iterator/go"
-	"github.com/mmcdole/gofeed/internal/shared"
+	"github.com/mmcdole/gofeed/v2/internal/shared"
 	xpp "github.com/mmcdole/goxpp"
 )
 
@@ -72,7 +72,7 @@ func DetectFeedType(feed io.Reader) FeedType {
 		}
 	} else if firstChar == '{' {
 		// Check if document is valid JSON
-		if jsoniter.Valid(buffer.Bytes()) {
+		if json.Valid(buffer.Bytes()) {
 			return FeedTypeJSON
 		}
 	}
diff --git a/extension_helpers.go b/extension_helpers.go
index 5ded1be..3babc69 100644
--- a/extension_helpers.go
+++ b/extension_helpers.go
@@ -1,7 +1,7 @@
 package gofeed
 
 import (
-	ext "github.com/mmcdole/gofeed/extensions"
+	ext "github.com/mmcdole/gofeed/v2/extensions"
 )
 
 // GetExtension retrieves extension values by namespace and element name.
diff --git a/feed.go b/feed.go
index 6c50428..0eb5c67 100644
--- a/feed.go
+++ b/feed.go
@@ -4,7 +4,7 @@ import (
 	"encoding/json"
 	"time"
 
-	ext "github.com/mmcdole/gofeed/extensions"
+	ext "github.com/mmcdole/gofeed/v2/extensions"
 )
 
 // Feed is the universal Feed type that atom.Feed
diff --git a/go.mod b/go.mod
index 326fcef..8fab7f6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,23 +1,18 @@
-module github.com/mmcdole/gofeed
+module github.com/mmcdole/gofeed/v2
 
-go 1.19
+go 1.21
 
 require (
-	github.com/PuerkitoBio/goquery v1.8.0
-	github.com/json-iterator/go v1.1.12
 	github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23
-	github.com/stretchr/testify v1.8.1
+	github.com/stretchr/testify v1.10.0
 	github.com/urfave/cli v1.22.3
-	golang.org/x/net v0.4.0
-	golang.org/x/text v0.5.0
+	golang.org/x/net v0.17.0
+	golang.org/x/text v0.14.0
 )
 
 require (
-	github.com/andybalholm/cascadia v1.3.1 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/russross/blackfriday/v2 v2.0.1 // indirect
 	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 62f5ad7..3169f48 100644
--- a/go.sum
+++ b/go.sum
@@ -1,52 +1,26 @@
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
-github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
-github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
-github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=
 github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
 github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
-golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
\ No newline at end of file
diff --git a/internal/shared/extparser.go b/internal/shared/extparser.go
index cb522b1..1d6bbfc 100644
--- a/internal/shared/extparser.go
+++ b/internal/shared/extparser.go
@@ -3,7 +3,7 @@ package shared
 import (
 	"strings"
 
-	"github.com/mmcdole/gofeed/extensions"
+	"github.com/mmcdole/gofeed/v2/extensions"
 	"github.com/mmcdole/goxpp"
 )
 
diff --git a/internal/shared/htmlutils.go b/internal/shared/htmlutils.go
new file mode 100644
index 0000000..481bfff
--- /dev/null
+++ b/internal/shared/htmlutils.go
@@ -0,0 +1,99 @@
+package shared
+
+import (
+	"bytes"
+	"strings"
+
+	"golang.org/x/net/html"
+)
+
+// FindFirstImgSrc finds the first <img> tag with a src attribute in the HTML document
+// and returns the src value. Returns empty string if no img with src is found.
+func FindFirstImgSrc(document string) string {
+	doc, err := html.Parse(strings.NewReader(document))
+	if err != nil {
+		return ""
+	}
+
+	var findImg func(*html.Node) string
+	findImg = func(n *html.Node) string {
+		if n.Type == html.ElementNode && n.Data == "img" {
+			for _, attr := range n.Attr {
+				if attr.Key == "src" {
+					return attr.Val
+				}
+			}
+		}
+		for c := n.FirstChild; c != nil; c = c.NextSibling {
+			if src := findImg(c); src != "" {
+				return src
+			}
+		}
+		return ""
+	}
+
+	return findImg(doc)
+}
+
+// StripWrappingDiv removes a single wrapping <div> element from HTML content
+// if the body contains only that div (and optional whitespace).
+// Returns the inner content of the div, or the original content if conditions aren't met.
+func StripWrappingDiv(content string) string {
+	doc, err := html.Parse(strings.NewReader(content))
+	if err != nil {
+		return content
+	}
+
+	// Find body node
+	var findBody func(*html.Node) *html.Node
+	findBody = func(n *html.Node) *html.Node {
+		if n.Type == html.ElementNode && n.Data == "body" {
+			return n
+		}
+		for c := n.FirstChild; c != nil; c = c.NextSibling {
+			if b := findBody(c); b != nil {
+				return b
+			}
+		}
+		return nil
+	}
+
+	body := findBody(doc)
+	if body == nil {
+		return content
+	}
+
+	// Find first non-whitespace child
+	var firstElement *html.Node
+	for c := body.FirstChild; c != nil; c = c.NextSibling {
+		if c.Type == html.ElementNode {
+			firstElement = c
+			break
+		}
+		if c.Type == html.TextNode && strings.TrimSpace(c.Data) != "" {
+			// Non-whitespace text node before any element
+			return content
+		}
+	}
+
+	if firstElement == nil || firstElement.Data != "div" {
+		return content
+	}
+
+	// Check no element siblings after the div
+	for c := firstElement.NextSibling; c != nil; c = c.NextSibling {
+		if c.Type == html.ElementNode {
+			return content // Has element siblings
+		}
+		if c.Type == html.TextNode && strings.TrimSpace(c.Data) != "" {
+			return content // Has non-whitespace text siblings
+		}
+	}
+
+	// Return inner HTML of the div
+	var buf bytes.Buffer
+	for c := firstElement.FirstChild; c != nil; c = c.NextSibling {
+		html.Render(&buf, c)
+	}
+	return buf.String()
+}
\ No newline at end of file
diff --git a/json/parser.go b/json/parser.go
index 5440cd4..20a340c 100644
--- a/json/parser.go
+++ b/json/parser.go
@@ -2,9 +2,8 @@ package json
 
 import (
 	"bytes"
+	"encoding/json"
 	"io"
-
-	jsoniter "github.com/json-iterator/go"
 )
 
 
@@ -18,8 +17,7 @@ func (ap *Parser) Parse(feed io.Reader) (*Feed, error) {
 	buffer := new(bytes.Buffer)
 	buffer.ReadFrom(feed)
 
-	j := jsoniter.ConfigCompatibleWithStandardLibrary
-	err := j.Unmarshal(buffer.Bytes(), jsonFeed)
+	err := json.Unmarshal(buffer.Bytes(), jsonFeed)
 	if err != nil {
 		return nil, err
 	}
diff --git a/parser.go b/parser.go
index 337ce02..f9e8bb8 100644
--- a/parser.go
+++ b/parser.go
@@ -9,9 +9,9 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/mmcdole/gofeed/atom"
-	"github.com/mmcdole/gofeed/json"
-	"github.com/mmcdole/gofeed/rss"
+	"github.com/mmcdole/gofeed/v2/atom"
+	"github.com/mmcdole/gofeed/v2/json"
+	"github.com/mmcdole/gofeed/v2/rss"
 )
 
 // ErrFeedTypeNotDetected is returned when the detection system can not figure
diff --git a/rss/feed.go b/rss/feed.go
index e8ebfe4..093be02 100644
--- a/rss/feed.go
+++ b/rss/feed.go
@@ -4,7 +4,7 @@ import (
 	"encoding/json"
 	"time"
 
-	ext "github.com/mmcdole/gofeed/extensions"
+	ext "github.com/mmcdole/gofeed/v2/extensions"
 )
 
 // Feed is an RSS Feed
diff --git a/rss/parser.go b/rss/parser.go
index 2f0223e..be7c1aa 100644
--- a/rss/parser.go
+++ b/rss/parser.go
@@ -5,8 +5,8 @@ import (
 	"io"
 	"strings"
 
-	ext "github.com/mmcdole/gofeed/extensions"
-	"github.com/mmcdole/gofeed/internal/shared"
+	ext "github.com/mmcdole/gofeed/v2/extensions"
+	"github.com/mmcdole/gofeed/v2/internal/shared"
 	xpp "github.com/mmcdole/goxpp"
 )
 
diff --git a/translator.go b/translator.go
index 5de162e..fea18b4 100644
--- a/translator.go
+++ b/translator.go
@@ -1,18 +1,15 @@
 package gofeed
 
 import (
-	"bytes"
 	"fmt"
 	"strings"
 	"time"
 
-	"github.com/PuerkitoBio/goquery"
-	"github.com/mmcdole/gofeed/atom"
-	ext "github.com/mmcdole/gofeed/extensions"
-	"github.com/mmcdole/gofeed/internal/shared"
-	"github.com/mmcdole/gofeed/json"
-	"github.com/mmcdole/gofeed/rss"
-	"golang.org/x/net/html"
+	"github.com/mmcdole/gofeed/v2/atom"
+	ext "github.com/mmcdole/gofeed/v2/extensions"
+	"github.com/mmcdole/gofeed/v2/internal/shared"
+	"github.com/mmcdole/gofeed/v2/json"
+	"github.com/mmcdole/gofeed/v2/rss"
 )
 
 // Translator converts a particular feed (atom.Feed or rss.Feed of json.Feed)
@@ -441,16 +438,9 @@ func (t *DefaultRSSTranslator) translateItemImage(rssItem *rss.Item) *Image {
 }
 
 func firstImageFromHtmlDocument(document string) *Image {
-	if doc, err := html.Parse(bytes.NewBufferString(document)); err == nil {
-		doc := goquery.NewDocumentFromNode(doc)
-		for _, node := range doc.FindMatcher(goquery.Single("img[src]")).Nodes {
-			for _, attr := range node.Attr {
-				if attr.Key == "src" {
-					return &Image{
-						URL: attr.Val,
-					}
-				}
-			}
+	if src := shared.FindFirstImgSrc(document); src != "" {
+		return &Image{
+			URL: src,
 		}
 	}
 	return nil



Test patch:
diff --git a/atom/parser_test.go b/atom/parser_test.go
index f9e3bd1..3e479e9 100644
--- a/atom/parser_test.go
+++ b/atom/parser_test.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mmcdole/gofeed/atom"
+	"github.com/mmcdole/gofeed/v2/atom"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/cmd/ftest/main.go b/cmd/ftest/main.go
index d2df2c9..040bba6 100644
--- a/cmd/ftest/main.go
+++ b/cmd/ftest/main.go
@@ -7,9 +7,9 @@ import (
 	"os"
 	"strings"
 
-	"github.com/mmcdole/gofeed"
-	"github.com/mmcdole/gofeed/atom"
-	"github.com/mmcdole/gofeed/rss"
+	"github.com/mmcdole/gofeed/v2"
+	"github.com/mmcdole/gofeed/v2/atom"
+	"github.com/mmcdole/gofeed/v2/rss"
 	"github.com/urfave/cli"
 )
 
diff --git a/detector_test.go b/detector_test.go
index 8105c84..8fbebd7 100644
--- a/detector_test.go
+++ b/detector_test.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mmcdole/gofeed"
+	"github.com/mmcdole/gofeed/v2"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/extension_helpers_test.go b/extension_helpers_test.go
index 12eb87a..1a96698 100644
--- a/extension_helpers_test.go
+++ b/extension_helpers_test.go
@@ -3,7 +3,7 @@ package gofeed
 import (
 	"testing"
 
-	ext "github.com/mmcdole/gofeed/extensions"
+	ext "github.com/mmcdole/gofeed/v2/extensions"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/extensions/extensions_test.go b/extensions/extensions_test.go
index 3edecab..404074c 100644
--- a/extensions/extensions_test.go
+++ b/extensions/extensions_test.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mmcdole/gofeed"
+	"github.com/mmcdole/gofeed/v2"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/feed_test.go b/feed_test.go
index 7baa8df..784125d 100644
--- a/feed_test.go
+++ b/feed_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/mmcdole/gofeed"
+	"github.com/mmcdole/gofeed/v2"
 )
 
 func TestFeedSort(t *testing.T) {
diff --git a/internal/shared/htmlutils_test.go b/internal/shared/htmlutils_test.go
new file mode 100644
index 0000000..84ef8f3
--- /dev/null
+++ b/internal/shared/htmlutils_test.go
@@ -0,0 +1,151 @@
+package shared
+
+import (
+	"testing"
+)
+
+func TestFindFirstImgSrc(t *testing.T) {
+	tests := []struct {
+		name     string
+		html     string
+		expected string
+	}{
+		{
+			name:     "simple img tag",
+			html:     `<img src="test.jpg">`,
+			expected: "test.jpg",
+		},
+		{
+			name:     "img tag with other attributes",
+			html:     `<img alt="test" src="image.png" width="100">`,
+			expected: "image.png",
+		},
+		{
+			name:     "nested img tag",
+			html:     `<div><p><img src="nested.gif"></p></div>`,
+			expected: "nested.gif",
+		},
+		{
+			name:     "multiple img tags",
+			html:     `<img src="first.jpg"><img src="second.jpg">`,
+			expected: "first.jpg",
+		},
+		{
+			name:     "img without src",
+			html:     `<img alt="no source">`,
+			expected: "",
+		},
+		{
+			name:     "no img tag",
+			html:     `<div><p>No images here</p></div>`,
+			expected: "",
+		},
+		{
+			name:     "empty html",
+			html:     ``,
+			expected: "",
+		},
+		{
+			name:     "invalid html",
+			html:     `<div><img src="test.jpg"`,
+			expected: "",
+		},
+		{
+			name:     "img in html with body",
+			html:     `<html><body><img src="body-image.jpg"></body></html>`,
+			expected: "body-image.jpg",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := FindFirstImgSrc(tt.html)
+			if result != tt.expected {
+				t.Errorf("FindFirstImgSrc() = %v, want %v", result, tt.expected)
+			}
+		})
+	}
+}
+
+func TestStripWrappingDiv(t *testing.T) {
+	tests := []struct {
+		name     string
+		html     string
+		expected string
+	}{
+		{
+			name:     "simple wrapping div",
+			html:     `<div>content</div>`,
+			expected: `content`,
+		},
+		{
+			name:     "div with nested content",
+			html:     `<div><p>paragraph</p><span>span</span></div>`,
+			expected: `<p>paragraph</p><span>span</span>`,
+		},
+		{
+			name:     "div with attributes",
+			html:     `<div class="wrapper">content</div>`,
+			expected: `content`,
+		},
+		{
+			name:     "multiple root elements",
+			html:     `<div>first</div><div>second</div>`,
+			expected: `<div>first</div><div>second</div>`,
+		},
+		{
+			name:     "non-div root element",
+			html:     `<p>paragraph</p>`,
+			expected: `<p>paragraph</p>`,
+		},
+		{
+			name:     "div with text sibling",
+			html:     `<div>content</div>extra text`,
+			expected: `<div>content</div>extra text`,
+		},
+		{
+			name:     "empty content",
+			html:     ``,
+			expected: ``,
+		},
+		{
+			name:     "only whitespace",
+			html:     `   `,
+			expected: `   `,
+		},
+		{
+			name:     "div with only whitespace siblings",
+			html:     `  <div>content</div>  `,
+			expected: `content`,
+		},
+		{
+			name:     "full html document with single div in body",
+			html:     `<html><body><div>content</div></body></html>`,
+			expected: `content`,
+		},
+		{
+			name:     "full html document with multiple elements",
+			html:     `<html><body><div>first</div><p>second</p></body></html>`,
+			expected: `<html><body><div>first</div><p>second</p></body></html>`,
+		},
+		{
+			name:     "nested divs",
+			html:     `<div><div>nested</div></div>`,
+			expected: `<div>nested</div>`,
+		},
+		{
+			name:     "invalid html - unclosed div gets auto-closed",
+			html:     `<div>unclosed`,
+			expected: `unclosed`, // HTML parser auto-closes the div
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := StripWrappingDiv(tt.html)
+			if result != tt.expected {
+				t.Errorf("StripWrappingDiv() = %q, want %q", result, tt.expected)
+			}
+		})
+	}
+}
\ No newline at end of file
diff --git a/json/parser_test.go b/json/parser_test.go
index ea65941..2420434 100644
--- a/json/parser_test.go
+++ b/json/parser_test.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"testing"
 
-	jsonParser "github.com/mmcdole/gofeed/json"
+	jsonParser "github.com/mmcdole/gofeed/v2/json"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -66,7 +66,7 @@ func TestParser_ParseInvalidAndStruct(t *testing.T) {
 	// Parse actual feed
 	fp := &jsonParser.Parser{}
 	_, err := fp.Parse(bytes.NewReader(f))
-	assert.Contains(t, err.Error(), "expect }")
+	assert.Contains(t, err.Error(), "unexpected end of JSON input")
 
 	name = "version_json_10"
 	fmt.Printf("Testing %s... ", name)
diff --git a/parser_test.go b/parser_test.go
index c85042d..76a6f7c 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -14,7 +14,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/mmcdole/gofeed"
+	"github.com/mmcdole/gofeed/v2"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/rss/parser_test.go b/rss/parser_test.go
index 51af578..054c9f1 100644
--- a/rss/parser_test.go
+++ b/rss/parser_test.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mmcdole/gofeed/rss"
+	"github.com/mmcdole/gofeed/v2/rss"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/translator_test.go b/translator_test.go
index d077aef..26aac5e 100644
--- a/translator_test.go
+++ b/translator_test.go
@@ -8,10 +8,10 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mmcdole/gofeed"
-	"github.com/mmcdole/gofeed/atom"
-	"github.com/mmcdole/gofeed/json"
-	"github.com/mmcdole/gofeed/rss"
+	"github.com/mmcdole/gofeed/v2"
+	"github.com/mmcdole/gofeed/v2/atom"
+	"github.com/mmcdole/gofeed/v2/json"
+	"github.com/mmcdole/gofeed/v2/rss"
 	"github.com/stretchr/testify/assert"
 )