Repository: mmcdole/gofeed (go)

Issue:
v2: Add constructors for format-specific parsers
## Overview

Add public constructors for format-specific parsers (RSS, Atom, JSON) and update the universal parser to use them, providing better encapsulation and API consistency.

## Tasks

### Add Public Constructors
- [x] Add constructor functions for each format:
  - `atom.NewParser() *Parser`
  - `rss.NewParser() *Parser`
  - `json.NewParser() *Parser`
- [x] Even if initially simple, provides API stability and proper initialization

### Update Universal Parser
- [x] Update `gofeed.Parser` to use constructors instead of struct literals:
  ```go
  // Current
  rp: &rss.Parser{}
  
  // New
  rp: rss.NewParser()
  ```
- [x] Remove direct struct initialization of parsers throughout codebase

### Benefits
- Better encapsulation - parsers can manage their own initialization
- API stability - can add initialization logic later without breaking changes
- Consistency - all parsers created the same way
- Cleaner separation between packages

## Example Usage
```go
// Format-specific parsing
parser := rss.NewParser()
feed, err := parser.Parse(reader, nil)

// Universal parser (internally uses constructors)
parser := gofeed.NewParser()
feed, err := parser.Parse(reader, nil)
```

## Related Issues
- Part of v2 RFC (#241)

Pull request description:
v2: Add constructors for format-specific parsers
## Overview

Adds constructor functions for format-specific parsers as specified in #248.

## Changes

- Added `NewParser()` for RSS, Atom, and JSON parsers
- Updated universal parser to use constructors instead of struct literals
- Removed misleading MaxItems (it didn't actually save memory)

## Breaking Changes

- MaxItems removed from ParseOptions

Closes #248

Patch:
diff --git a/atom/parser.go b/atom/parser.go
index 903a6de..90b2475 100644
--- a/atom/parser.go
+++ b/atom/parser.go
@@ -25,8 +25,13 @@ var (
 // Parser is an Atom Parser
 type Parser struct{}
 
+// NewParser creates a new Atom parser
+func NewParser() *Parser {
+	return &Parser{}
+}
+
 // Parse parses an xml feed into an atom.Feed
-func (ap *Parser) Parse(feed io.Reader) (*Feed, error) {
+func (ap *Parser) Parse(feed io.Reader, opts *shared.ParseOptions) (*Feed, error) {
 	p := xpp.NewXMLPullParser(feed, false, shared.NewReaderLabel)
 
 	_, err := shared.FindRoot(p)
@@ -34,10 +39,10 @@ func (ap *Parser) Parse(feed io.Reader) (*Feed, error) {
 		return nil, err
 	}
 
-	return ap.parseRoot(p)
+	return ap.parseRoot(p, opts)
 }
 
-func (ap *Parser) parseRoot(p *xpp.XMLPullParser) (*Feed, error) {
+func (ap *Parser) parseRoot(p *xpp.XMLPullParser, opts *shared.ParseOptions) (*Feed, error) {
 	if err := p.Expect(xpp.StartTag, "feed"); err != nil {
 		return nil, err
 	}
diff --git a/feed.go b/feed.go
index 1eb0c55..785832f 100644
--- a/feed.go
+++ b/feed.go
@@ -35,6 +35,9 @@ type Feed struct {
 	Items           []*Item                  `json:"items"`
 	FeedType        string                   `json:"feedType"`
 	FeedVersion     string                   `json:"feedVersion"`
+	
+	// Original format-specific feed data (only populated if KeepOriginalFeed is true)
+	OriginalFeed interface{} `json:"-"`
 }
 
 // String returns a JSON representation of the Feed for debugging purposes.
diff --git a/internal/shared/parse_options.go b/internal/shared/parse_options.go
new file mode 100644
index 0000000..17608a3
--- /dev/null
+++ b/internal/shared/parse_options.go
@@ -0,0 +1,55 @@
+package shared
+
+import (
+	"net/http"
+	"time"
+)
+
+// ParseOptions configures how feeds are parsed
+type ParseOptions struct {
+	// Keep reference to the original format-specific feed
+	KeepOriginalFeed bool
+	
+	// Whether to parse dates (can be disabled for performance)
+	ParseDates bool
+	
+	// Parsing behavior options
+	StrictnessOptions StrictnessOptions
+	
+	// HTTP request configuration for ParseURL
+	RequestOptions RequestOptions
+}
+
+// StrictnessOptions controls parsing strictness
+type StrictnessOptions struct {
+	AllowInvalidDates     bool
+	AllowMissingRequired  bool
+	AllowUnescapedMarkup  bool
+}
+
+// RequestOptions configures HTTP requests for ParseURL
+type RequestOptions struct {
+	UserAgent        string
+	Timeout          time.Duration
+	IfNoneMatch      string     // ETag for conditional requests
+	IfModifiedSince  time.Time  // For conditional requests
+	Client           *http.Client
+	AuthConfig       interface{} // Will be *Auth from parent package
+}
+
+// DefaultParseOptions returns sensible defaults
+func DefaultParseOptions() *ParseOptions {
+	return &ParseOptions{
+		KeepOriginalFeed: false,
+		ParseDates: true,
+		StrictnessOptions: StrictnessOptions{
+			AllowInvalidDates:     true,
+			AllowMissingRequired:  true,
+			AllowUnescapedMarkup:  true,
+		},
+		RequestOptions: RequestOptions{
+			UserAgent: "gofeed/2.0 (+https://github.com/mmcdole/gofeed)",
+			Timeout:   60 * time.Second,
+		},
+	}
+}
\ No newline at end of file
diff --git a/json/parser.go b/json/parser.go
index 20a340c..1ca1198 100644
--- a/json/parser.go
+++ b/json/parser.go
@@ -4,14 +4,21 @@ import (
 	"bytes"
 	"encoding/json"
 	"io"
+	
+	"github.com/mmcdole/gofeed/v2/internal/shared"
 )
 
 
 // Parser is an JSON Feed Parser
 type Parser struct{}
 
+// NewParser creates a new JSON Feed parser
+func NewParser() *Parser {
+	return &Parser{}
+}
+
 // Parse parses an json feed into an json.Feed
-func (ap *Parser) Parse(feed io.Reader) (*Feed, error) {
+func (ap *Parser) Parse(feed io.Reader, opts *shared.ParseOptions) (*Feed, error) {
 	jsonFeed := &Feed{}
 
 	buffer := new(bytes.Buffer)
@@ -21,5 +28,6 @@ func (ap *Parser) Parse(feed io.Reader) (*Feed, error) {
 	if err != nil {
 		return nil, err
 	}
-	return jsonFeed, err
+	
+	return jsonFeed, nil
 }
diff --git a/parse_options.go b/parse_options.go
new file mode 100644
index 0000000..c138c66
--- /dev/null
+++ b/parse_options.go
@@ -0,0 +1,24 @@
+package gofeed
+
+import (
+	"github.com/mmcdole/gofeed/v2/internal/shared"
+)
+
+// ParseOptions re-exports the shared type
+type ParseOptions = shared.ParseOptions
+
+// StrictnessOptions re-exports the shared type  
+type StrictnessOptions = shared.StrictnessOptions
+
+// RequestOptions re-exports the shared type
+type RequestOptions = shared.RequestOptions
+
+// DefaultParseOptions returns sensible defaults
+func DefaultParseOptions() *ParseOptions {
+	opts := shared.DefaultParseOptions()
+	// Fix the AuthConfig type to use our Auth type
+	if opts.RequestOptions.AuthConfig == nil {
+		opts.RequestOptions.AuthConfig = (*Auth)(nil)
+	}
+	return opts
+}
\ No newline at end of file
diff --git a/parser.go b/parser.go
index f9e8bb8..968ff9a 100644
--- a/parser.go
+++ b/parser.go
@@ -36,9 +36,6 @@ type Parser struct {
 	AtomTranslator Translator
 	RSSTranslator  Translator
 	JSONTranslator Translator
-	UserAgent      string
-	AuthConfig     *Auth
-	Client         *http.Client
 	rp             *rss.Parser
 	ap             *atom.Parser
 	jp             *json.Parser
@@ -55,10 +52,9 @@ type Auth struct {
 // NewParser creates a universal feed parser.
 func NewParser() *Parser {
 	fp := Parser{
-		rp:        &rss.Parser{},
-		ap:        &atom.Parser{},
-		jp:        &json.Parser{},
-		UserAgent: "Gofeed/1.0",
+		rp:        rss.NewParser(),
+		ap:        atom.NewParser(),
+		jp:        json.NewParser(),
 	}
 	return &fp
 }
@@ -66,7 +62,11 @@ func NewParser() *Parser {
 // Parse parses a RSS or Atom or JSON feed into
 // the universal gofeed.Feed.  It takes an
 // io.Reader which should return the xml/json content.
-func (f *Parser) Parse(feed io.Reader) (*Feed, error) {
+func (f *Parser) Parse(feed io.Reader, opts *ParseOptions) (*Feed, error) {
+	if opts == nil {
+		opts = DefaultParseOptions()
+	}
+	
 	// Wrap the feed io.Reader in a io.TeeReader
 	// so we can capture all the bytes read by the
 	// DetectFeedType function and construct a new
@@ -82,43 +82,50 @@ func (f *Parser) Parse(feed io.Reader) (*Feed, error) {
 
 	switch feedType {
 	case FeedTypeAtom:
-		return f.parseAtomFeed(r)
+		return f.parseAtomFeed(r, opts)
 	case FeedTypeRSS:
-		return f.parseRSSFeed(r)
+		return f.parseRSSFeed(r, opts)
 	case FeedTypeJSON:
-		return f.parseJSONFeed(r)
+		return f.parseJSONFeed(r, opts)
 	}
 
 	return nil, ErrFeedTypeNotDetected
 }
 
-// ParseURL fetches the contents of a given url and
-// attempts to parse the response into the universal feed type.
-func (f *Parser) ParseURL(feedURL string) (feed *Feed, err error) {
-	return f.ParseURLWithContext(feedURL, context.Background())
-}
+// ParseURL fetches the contents of a given url and attempts to parse
+// the response into the universal feed type. Context can be used to control
+// timeout and cancellation.
+func (f *Parser) ParseURL(ctx context.Context, feedURL string, opts *ParseOptions) (feed *Feed, err error) {
+	if opts == nil {
+		opts = DefaultParseOptions()
+	}
 
-// ParseURLWithContext fetches contents of a given url and
-// attempts to parse the response into the universal feed type.
-// You can instantiate the Auth structure with your Username and Password
-// to use the BasicAuth during the HTTP call.
-// It will be automatically added to the header of the request
-// Request could be canceled or timeout via given context
-func (f *Parser) ParseURLWithContext(feedURL string, ctx context.Context) (feed *Feed, err error) {
-	client := f.httpClient()
+	client := opts.RequestOptions.Client
+	if client == nil {
+		client = &http.Client{
+			Timeout: opts.RequestOptions.Timeout,
+		}
+	}
 
 	req, err := http.NewRequestWithContext(ctx, "GET", feedURL, nil)
 	if err != nil {
 		return nil, err
 	}
-	req.Header.Set("User-Agent", f.UserAgent)
+	
+	// Set user agent
+	if opts.RequestOptions.UserAgent != "" {
+		req.Header.Set("User-Agent", opts.RequestOptions.UserAgent)
+	}
 
-	if f.AuthConfig != nil && f.AuthConfig.Username != "" && f.AuthConfig.Password != "" {
-		req.SetBasicAuth(f.AuthConfig.Username, f.AuthConfig.Password)
+	// TODO: Implement conditional request support (IfNoneMatch, IfModifiedSince)
+	// This will be implemented as part of issue #247
+
+	// Set auth if provided
+	if auth, ok := opts.RequestOptions.AuthConfig.(*Auth); ok && auth != nil && auth.Username != "" && auth.Password != "" {
+		req.SetBasicAuth(auth.Username, auth.Password)
 	}
 
 	resp, err := client.Do(req)
-
 	if err != nil {
 		return nil, err
 	}
@@ -138,38 +145,68 @@ func (f *Parser) ParseURLWithContext(feedURL string, ctx context.Context) (feed
 		}
 	}
 
-	return f.Parse(resp.Body)
+	// Parse the feed
+	return f.Parse(resp.Body, opts)
 }
 
 // ParseString parses a feed XML string and into the
 // universal feed type.
-func (f *Parser) ParseString(feed string) (*Feed, error) {
-	return f.Parse(strings.NewReader(feed))
+func (f *Parser) ParseString(feed string, opts *ParseOptions) (*Feed, error) {
+	return f.Parse(strings.NewReader(feed), opts)
 }
 
-func (f *Parser) parseAtomFeed(feed io.Reader) (*Feed, error) {
-	af, err := f.ap.Parse(feed)
+func (f *Parser) parseAtomFeed(feed io.Reader, opts *ParseOptions) (*Feed, error) {
+	af, err := f.ap.Parse(feed, opts)
+	if err != nil {
+		return nil, err
+	}
+	
+	result, err := f.atomTrans().Translate(af, opts)
 	if err != nil {
 		return nil, err
 	}
-	return f.atomTrans().Translate(af)
+	
+	if opts != nil && opts.KeepOriginalFeed {
+		result.OriginalFeed = af
+	}
+	
+	return result, nil
 }
 
-func (f *Parser) parseRSSFeed(feed io.Reader) (*Feed, error) {
-	rf, err := f.rp.Parse(feed)
+func (f *Parser) parseRSSFeed(feed io.Reader, opts *ParseOptions) (*Feed, error) {
+	rf, err := f.rp.Parse(feed, opts)
 	if err != nil {
 		return nil, err
 	}
 
-	return f.rssTrans().Translate(rf)
+	result, err := f.rssTrans().Translate(rf, opts)
+	if err != nil {
+		return nil, err
+	}
+	
+	if opts != nil && opts.KeepOriginalFeed {
+		result.OriginalFeed = rf
+	}
+	
+	return result, nil
 }
 
-func (f *Parser) parseJSONFeed(feed io.Reader) (*Feed, error) {
-	jf, err := f.jp.Parse(feed)
+func (f *Parser) parseJSONFeed(feed io.Reader, opts *ParseOptions) (*Feed, error) {
+	jf, err := f.jp.Parse(feed, opts)
 	if err != nil {
 		return nil, err
 	}
-	return f.jsonTrans().Translate(jf)
+	
+	result, err := f.jsonTrans().Translate(jf, opts)
+	if err != nil {
+		return nil, err
+	}
+	
+	if opts != nil && opts.KeepOriginalFeed {
+		result.OriginalFeed = jf
+	}
+	
+	return result, nil
 }
 
 func (f *Parser) atomTrans() Translator {
@@ -196,10 +233,3 @@ func (f *Parser) jsonTrans() Translator {
 	return f.JSONTranslator
 }
 
-func (f *Parser) httpClient() *http.Client {
-	if f.Client != nil {
-		return f.Client
-	}
-	f.Client = &http.Client{}
-	return f.Client
-}
diff --git a/rss/parser.go b/rss/parser.go
index be7c1aa..5c72966 100644
--- a/rss/parser.go
+++ b/rss/parser.go
@@ -13,8 +13,13 @@ import (
 // Parser is a RSS Parser
 type Parser struct{}
 
+// NewParser creates a new RSS parser
+func NewParser() *Parser {
+	return &Parser{}
+}
+
 // Parse parses an xml feed into an rss.Feed
-func (rp *Parser) Parse(feed io.Reader) (*Feed, error) {
+func (rp *Parser) Parse(feed io.Reader, opts *shared.ParseOptions) (*Feed, error) {
 	p := xpp.NewXMLPullParser(feed, false, shared.NewReaderLabel)
 
 	_, err := shared.FindRoot(p)
@@ -22,10 +27,10 @@ func (rp *Parser) Parse(feed io.Reader) (*Feed, error) {
 		return nil, err
 	}
 
-	return rp.parseRoot(p)
+	return rp.parseRoot(p, opts)
 }
 
-func (rp *Parser) parseRoot(p *xpp.XMLPullParser) (*Feed, error) {
+func (rp *Parser) parseRoot(p *xpp.XMLPullParser, opts *shared.ParseOptions) (*Feed, error) {
 	rssErr := p.Expect(xpp.StartTag, "rss")
 	rdfErr := p.Expect(xpp.StartTag, "rdf")
 	if rssErr != nil && rdfErr != nil {
@@ -61,7 +66,7 @@ func (rp *Parser) parseRoot(p *xpp.XMLPullParser) (*Feed, error) {
 			name := strings.ToLower(p.Name)
 
 			if name == "channel" {
-				channel, err = rp.parseChannel(p)
+				channel, err = rp.parseChannel(p, opts)
 				if err != nil {
 					return nil, err
 				}
@@ -114,7 +119,7 @@ func (rp *Parser) parseRoot(p *xpp.XMLPullParser) (*Feed, error) {
 	return channel, nil
 }
 
-func (rp *Parser) parseChannel(p *xpp.XMLPullParser) (rss *Feed, err error) {
+func (rp *Parser) parseChannel(p *xpp.XMLPullParser, opts *shared.ParseOptions) (rss *Feed, err error) {
 	if err = p.Expect(xpp.StartTag, "channel"); err != nil {
 		return nil, err
 	}
diff --git a/translator.go b/translator.go
index fea18b4..4d3422a 100644
--- a/translator.go
+++ b/translator.go
@@ -15,7 +15,7 @@ import (
 // Translator converts a particular feed (atom.Feed or rss.Feed of json.Feed)
 // into the generic Feed struct
 type Translator interface {
-	Translate(feed interface{}) (*Feed, error)
+	Translate(feed interface{}, opts *ParseOptions) (*Feed, error)
 }
 
 // DefaultRSSTranslator converts an rss.Feed struct
@@ -28,11 +28,12 @@ type DefaultRSSTranslator struct{}
 
 // Translate converts an RSS feed into the universal
 // feed type.
-func (t *DefaultRSSTranslator) Translate(feed interface{}) (*Feed, error) {
+func (t *DefaultRSSTranslator) Translate(feed interface{}, opts *ParseOptions) (*Feed, error) {
 	rss, found := feed.(*rss.Feed)
 	if !found {
 		return nil, fmt.Errorf("Feed did not match expected type of *rss.Feed")
 	}
+	
 
 	result := &Feed{}
 	result.Title = t.translateFeedTitle(rss)
@@ -527,11 +528,12 @@ type DefaultAtomTranslator struct{}
 
 // Translate converts an Atom feed into the universal
 // feed type.
-func (t *DefaultAtomTranslator) Translate(feed interface{}) (*Feed, error) {
+func (t *DefaultAtomTranslator) Translate(feed interface{}, opts *ParseOptions) (*Feed, error) {
 	atom, found := feed.(*atom.Feed)
 	if !found {
 		return nil, fmt.Errorf("Feed did not match expected type of *atom.Feed")
 	}
+	
 
 	result := &Feed{}
 	result.Title = t.translateFeedTitle(atom)
@@ -855,7 +857,7 @@ type DefaultJSONTranslator struct{}
 
 // Translate converts an JSON feed into the universal
 // feed type.
-func (t *DefaultJSONTranslator) Translate(feed interface{}) (*Feed, error) {
+func (t *DefaultJSONTranslator) Translate(feed interface{}, opts *ParseOptions) (*Feed, error) {
 	json, found := feed.(*json.Feed)
 	if !found {
 		return nil, fmt.Errorf("Feed did not match expected type of *json.Feed")


Test patch:
diff --git a/atom/parser_test.go b/atom/parser_test.go
index 3e479e9..8b0440e 100644
--- a/atom/parser_test.go
+++ b/atom/parser_test.go
@@ -28,8 +28,8 @@ func TestParser_Parse(t *testing.T) {
 		f, _ := os.ReadFile(ff)
 
 		// Parse actual feed
-		fp := &atom.Parser{}
-		actual, _ := fp.Parse(bytes.NewReader(f))
+		fp := atom.NewParser()
+		actual, _ := fp.Parse(bytes.NewReader(f), nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("../testdata/parser/atom/%s.json", name)
diff --git a/cmd/ftest/main.go b/cmd/ftest/main.go
index 040bba6..31d9cff 100644
--- a/cmd/ftest/main.go
+++ b/cmd/ftest/main.go
@@ -44,14 +44,14 @@ func main() {
 		if strings.EqualFold(feedType, "rss") ||
 			strings.EqualFold(feedType, "r") {
 			p := rss.Parser{}
-			feed, err = p.Parse(strings.NewReader(fc))
+			feed, err = p.Parse(strings.NewReader(fc), nil)
 		} else if strings.EqualFold(feedType, "atom") ||
 			strings.EqualFold(feedType, "a") {
 			p := atom.Parser{}
-			feed, err = p.Parse(strings.NewReader(fc))
+			feed, err = p.Parse(strings.NewReader(fc), nil)
 		} else {
 			p := gofeed.NewParser()
-			feed, err = p.ParseString(fc)
+			feed, err = p.ParseString(fc, nil)
 		}
 
 		if err != nil {
diff --git a/extensions/extensions_test.go b/extensions/extensions_test.go
index 404074c..4f0eb9e 100644
--- a/extensions/extensions_test.go
+++ b/extensions/extensions_test.go
@@ -27,7 +27,7 @@ func TestITunes_Extensions(t *testing.T) {
 
 		// Parse actual feed
 		fp := gofeed.NewParser()
-		actual, _ := fp.Parse(bytes.NewReader(f))
+		actual, _ := fp.Parse(bytes.NewReader(f), nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("../testdata/extensions/itunes/%s.json", name)
@@ -60,7 +60,7 @@ func TestMedia_Extensions(t *testing.T) {
 
 		// Parse actual feed
 		fp := gofeed.NewParser()
-		actual, _ := fp.Parse(bytes.NewReader(f))
+		actual, _ := fp.Parse(bytes.NewReader(f), nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("../testdata/extensions/media/%s.json", name)
diff --git a/json/parser_test.go b/json/parser_test.go
index 2420434..f4b1ce9 100644
--- a/json/parser_test.go
+++ b/json/parser_test.go
@@ -34,8 +34,8 @@ func TestParser_Parse(t *testing.T) {
 		f, _ := os.ReadFile(ff)
 
 		// Parse actual feed
-		fp := &jsonParser.Parser{}
-		actual, _ := fp.Parse(bytes.NewReader(f))
+		fp := jsonParser.NewParser()
+		actual, _ := fp.Parse(bytes.NewReader(f), nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("../testdata/parser/json/%s_expected.json", name)
@@ -64,8 +64,8 @@ func TestParser_ParseInvalidAndStruct(t *testing.T) {
 	f, _ := os.ReadFile(ff)
 
 	// Parse actual feed
-	fp := &jsonParser.Parser{}
-	_, err := fp.Parse(bytes.NewReader(f))
+	fp := jsonParser.NewParser()
+	_, err := fp.Parse(bytes.NewReader(f), nil)
 	assert.Contains(t, err.Error(), "unexpected end of JSON input")
 
 	name = "version_json_10"
@@ -77,7 +77,7 @@ func TestParser_ParseInvalidAndStruct(t *testing.T) {
 	f, _ = os.ReadFile(ff)
 
 	// Parse actual feed
-	actual, _ := fp.Parse(bytes.NewReader(f))
+	actual, _ := fp.Parse(bytes.NewReader(f), nil)
 
 	assert.Equal(t, "1.0", actual.Version)
 	assert.Equal(t, "title", actual.Title)
diff --git a/parser_test.go b/parser_test.go
index 76a6f7c..e3d8d63 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -48,7 +48,7 @@ func TestParser_Parse(t *testing.T) {
 
 		// Get actual value
 		fp := gofeed.NewParser()
-		feed, err := fp.Parse(bytes.NewReader(f))
+		feed, err := fp.Parse(bytes.NewReader(f), nil)
 
 		if test.hasError {
 			assert.NotNil(t, err)
@@ -90,7 +90,7 @@ func TestParser_ParseString(t *testing.T) {
 
 		// Get actual value
 		fp := gofeed.NewParser()
-		feed, err := fp.ParseString(string(f))
+		feed, err := fp.ParseString(string(f), nil)
 
 		if test.hasError {
 			assert.NotNil(t, err)
@@ -133,8 +133,12 @@ func TestParser_ParseURL_Success(t *testing.T) {
 		// Get actual value
 		server, client := mockServerResponse(200, string(f), 0)
 		fp := gofeed.NewParser()
-		fp.Client = client
-		feed, err := fp.ParseURL(server.URL)
+		opts := &gofeed.ParseOptions{
+			RequestOptions: gofeed.RequestOptions{
+				Client: client,
+			},
+		}
+		feed, err := fp.ParseURL(context.Background(), server.URL, opts)
 
 		if test.hasError {
 			assert.NotNil(t, err)
@@ -153,16 +157,24 @@ func TestParser_ParseURLWithContext(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 	defer cancel()
 	fp := gofeed.NewParser()
-	fp.Client = client
-	_, err := fp.ParseURLWithContext(server.URL, ctx)
+	opts := &gofeed.ParseOptions{
+		RequestOptions: gofeed.RequestOptions{
+			Client: client,
+		},
+	}
+	_, err := fp.ParseURL(ctx, server.URL, opts)
 	assert.True(t, strings.Contains(err.Error(), ctx.Err().Error()))
 }
 
 func TestParser_ParseURL_Failure(t *testing.T) {
 	server, client := mockServerResponse(404, "", 0)
 	fp := gofeed.NewParser()
-	fp.Client = client
-	feed, err := fp.ParseURL(server.URL)
+	opts := &gofeed.ParseOptions{
+		RequestOptions: gofeed.RequestOptions{
+			Client: client,
+		},
+	}
+	feed, err := fp.ParseURL(context.Background(), server.URL, opts)
 
 	assert.NotNil(t, err)
 	assert.IsType(t, gofeed.HTTPError{}, err)
@@ -174,12 +186,16 @@ func TestParser_ParseURLWithContextAndBasicAuth(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 	defer cancel()
 	fp := gofeed.NewParser()
-	fp.AuthConfig = &gofeed.Auth{
-		Username: "foo",
-		Password: "bar",
+	opts := &gofeed.ParseOptions{
+		RequestOptions: gofeed.RequestOptions{
+			Client: client,
+			AuthConfig: &gofeed.Auth{
+				Username: "foo",
+				Password: "bar",
+			},
+		},
 	}
-	fp.Client = client
-	_, err := fp.ParseURLWithContext(server.URL, ctx)
+	_, err := fp.ParseURL(ctx, server.URL, opts)
 	assert.True(t, strings.Contains(err.Error(), ctx.Err().Error()))
 }
 
@@ -205,7 +221,7 @@ func TestParser_Concurrent(t *testing.T) {
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
-			fp.ParseString(string(f))
+			fp.ParseString(string(f), nil)
 		}()
 	}
 	wg.Wait()
@@ -240,7 +256,7 @@ func ExampleParser_Parse() {
 </channel>
 </rss>`
 	fp := gofeed.NewParser()
-	feed, err := fp.Parse(strings.NewReader(feedData))
+	feed, err := fp.Parse(strings.NewReader(feedData), nil)
 	if err != nil {
 		panic(err)
 	}
@@ -249,7 +265,7 @@ func ExampleParser_Parse() {
 
 func ExampleParser_ParseURL() {
 	fp := gofeed.NewParser()
-	feed, err := fp.ParseURL("http://feeds.twit.tv/twit.xml")
+	feed, err := fp.ParseURL(context.Background(), "http://feeds.twit.tv/twit.xml", nil)
 	if err != nil {
 		panic(err)
 	}
@@ -263,7 +279,7 @@ func ExampleParser_ParseString() {
 </channel>
 </rss>`
 	fp := gofeed.NewParser()
-	feed, err := fp.ParseString(feedData)
+	feed, err := fp.ParseString(feedData, nil)
 	if err != nil {
 		panic(err)
 	}
@@ -272,11 +288,15 @@ func ExampleParser_ParseString() {
 
 func ExampleParser_ParseURL_withAuth() {
 	fp := gofeed.NewParser()
-	fp.AuthConfig = &gofeed.Auth{
-		Username: "foo",
-		Password: "bar",
+	opts := &gofeed.ParseOptions{
+		RequestOptions: gofeed.RequestOptions{
+			AuthConfig: &gofeed.Auth{
+				Username: "foo",
+				Password: "bar",
+			},
+		},
 	}
-	feed, err := fp.ParseURL("http://feeds.twit.tv/twit.xml")
+	feed, err := fp.ParseURL(context.Background(), "http://feeds.twit.tv/twit.xml", opts)
 	if err != nil {
 		panic(err)
 	}
diff --git a/rss/parser_test.go b/rss/parser_test.go
index 054c9f1..d98983f 100644
--- a/rss/parser_test.go
+++ b/rss/parser_test.go
@@ -26,8 +26,8 @@ func TestParser_Parse(t *testing.T) {
 		f, _ := os.ReadFile(ff)
 
 		// Parse actual feed
-		fp := &rss.Parser{}
-		actual, _ := fp.Parse(bytes.NewReader(f))
+		fp := rss.NewParser()
+		actual, _ := fp.Parse(bytes.NewReader(f), nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("../testdata/parser/rss/%s.json", name)
diff --git a/translator_test.go b/translator_test.go
index 26aac5e..e93d0ea 100644
--- a/translator_test.go
+++ b/translator_test.go
@@ -30,9 +30,9 @@ func TestDefaultRSSTranslator_Translate(t *testing.T) {
 
 		// Parse actual feed
 		translator := &gofeed.DefaultRSSTranslator{}
-		fp := &rss.Parser{}
-		rssFeed, _ := fp.Parse(f)
-		actual, _ := translator.Translate(rssFeed)
+		fp := rss.NewParser()
+		rssFeed, _ := fp.Parse(f, nil)
+		actual, _ := translator.Translate(rssFeed, nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("testdata/translator/rss/%s.json", name)
@@ -52,7 +52,7 @@ func TestDefaultRSSTranslator_Translate(t *testing.T) {
 
 func TestDefaultRSSTranslator_Translate_WrongType(t *testing.T) {
 	translator := &gofeed.DefaultRSSTranslator{}
-	af, err := translator.Translate("wrong type")
+	af, err := translator.Translate("wrong type", nil)
 	assert.Nil(t, af)
 	assert.NotNil(t, err)
 }
@@ -72,9 +72,9 @@ func TestDefaultAtomTranslator_Translate(t *testing.T) {
 
 		// Parse actual feed
 		translator := &gofeed.DefaultAtomTranslator{}
-		fp := &atom.Parser{}
-		atomFeed, _ := fp.Parse(f)
-		actual, _ := translator.Translate(atomFeed)
+		fp := atom.NewParser()
+		atomFeed, _ := fp.Parse(f, nil)
+		actual, _ := translator.Translate(atomFeed, nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("testdata/translator/atom/%s.json", name)
@@ -94,7 +94,7 @@ func TestDefaultAtomTranslator_Translate(t *testing.T) {
 
 func TestDefaultAtomTranslator_Translate_WrongType(t *testing.T) {
 	translator := &gofeed.DefaultAtomTranslator{}
-	af, err := translator.Translate("wrong type")
+	af, err := translator.Translate("wrong type", nil)
 	assert.Nil(t, af)
 	assert.NotNil(t, err)
 }
@@ -118,9 +118,9 @@ func TestDefaultJSONTranslator_Translate(t *testing.T) {
 
 		// Parse actual feed
 		translator := &gofeed.DefaultJSONTranslator{}
-		fp := json.Parser{}
-		jsonFeed, _ := fp.Parse(f)
-		actual, _ := translator.Translate(jsonFeed)
+		fp := json.NewParser()
+		jsonFeed, _ := fp.Parse(f, nil)
+		actual, _ := translator.Translate(jsonFeed, nil)
 
 		// Get json encoded expected feed result
 		ef := fmt.Sprintf("testdata/translator/json/%s_expected.json", name)
@@ -151,9 +151,9 @@ func TestDefaultJSONTranslator_Translate(t *testing.T) {
 
 	// Parse actual feed
 	translator := &gofeed.DefaultJSONTranslator{}
-	fp := json.Parser{}
-	feed, _ := fp.Parse(bytes.NewReader(f))
-	actual, _ := translator.Translate(feed)
+	fp := json.NewParser()
+	feed, _ := fp.Parse(bytes.NewReader(f), nil)
+	actual, _ := translator.Translate(feed, nil)
 
 	assert.Equal(t, "title", actual.Title)
 	assert.Equal(t, "description", actual.Description)
@@ -207,8 +207,8 @@ func TestDefaultJSONTranslator_Translate(t *testing.T) {
 	f, _ = ioutil.ReadFile(ff)
 
 	// Parse actual feed
-	feed, _ = fp.Parse(bytes.NewReader(f))
-	actual, _ = translator.Translate(feed)
+	feed, _ = fp.Parse(bytes.NewReader(f), nil)
+	actual, _ = translator.Translate(feed, nil)
 
 	assert.Equal(t, "content_text", actual.Items[0].Content)
 	assert.Equal(t, "https://sample-json-feed.com/banner_image.png", actual.Items[0].Image.URL)
@@ -218,7 +218,7 @@ func TestDefaultJSONTranslator_Translate(t *testing.T) {
 
 func TestDefaultJSONTranslator_Translate_WrongType(t *testing.T) {
 	translator := &gofeed.DefaultJSONTranslator{}
-	af, err := translator.Translate("wrong type")
+	af, err := translator.Translate("wrong type", nil)
 	assert.Nil(t, af)
 	assert.NotNil(t, err)
 }