diff --git a/README.md b/README.md index 4d26bfe..f628e8a 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,45 @@ options := xmlquery.ParserOptions{ doc, err := xmlquery.ParseWithOptions(utf8Reader, options) ``` +### Parse non-ASCII characters and custom entity names. + +UTF-8 XML can contain characters such as `ä` directly, or use a numeric +character reference such as `ä` or `ä`. These work with `Parse()`. +HTML entity names such as `ä` are not predefined in XML, so `Parse()` +reports an unknown entity. A character encoding conversion does not define +those entity names. + +If the input uses known custom names, provide their replacements through +`DecoderOptions.Entity`: + +```go +s := `ä & ä` +options := xmlquery.ParserOptions{ + Decoder: &xmlquery.DecoderOptions{ + Strict: true, + Entity: map[string]string{"auml": "ä"}, + }, +} +doc, err := xmlquery.ParseWithOptions(strings.NewReader(s), options) +if err != nil { + panic(err) +} +fmt.Println(xmlquery.FindOne(doc, "//name").InnerText()) // ä & ä +``` + +Set `Strict: true` explicitly when supplying `DecoderOptions`: its zero value +is false, unlike the default XML decoder. This keeps malformed XML and unknown +entity names as errors. The entity map above defines only `auml`; it does not +load an HTML entity set or declarations from a DTD. + +For XML bytes in another character encoding, such as ISO-8859-1, use +`DecoderOptions.CharsetReader`. `Parse()` already uses +`golang.org/x/net/html/charset.NewReaderLabel` for an XML encoding declaration. +Supplying `DecoderOptions` replaces these decoder defaults, so when combining +custom entities with a non-UTF-8 document, also set +`CharsetReader: charset.NewReaderLabel` (from `golang.org/x/net/html/charset`). +An entity map and a charset reader solve different problems. + ### Query with custom namespace prefix. ```go diff --git a/doc_test.go b/doc_test.go index ce1e903..e3c485b 100644 --- a/doc_test.go +++ b/doc_test.go @@ -6,6 +6,7 @@ import ( "github.com/antchfx/xmlquery" "github.com/antchfx/xpath" + "golang.org/x/net/html/charset" ) func Example() { @@ -62,3 +63,51 @@ func Example() { prices := xpath.MustCompile(`sum(//book/price)`).Evaluate(nav).(float64) fmt.Println(prices) // > Output: 56.85 } + +func ExampleParseWithOptions_customEntities() { + s := `ä & ä` + options := xmlquery.ParserOptions{ + Decoder: &xmlquery.DecoderOptions{ + Strict: true, + Entity: map[string]string{"auml": "ä"}, + }, + } + doc, err := xmlquery.ParseWithOptions(strings.NewReader(s), options) + if err != nil { + panic(err) + } + fmt.Println(xmlquery.FindOne(doc, "//name").InnerText()) + + for _, input := range []string{`ä`, `ä`, `ä`} { + doc, err := xmlquery.Parse(strings.NewReader(input)) + if err != nil { + panic(err) + } + fmt.Println(xmlquery.FindOne(doc, "//name").InnerText()) + } + + _, err = xmlquery.Parse(strings.NewReader(`ä`)) + fmt.Println(err != nil) + _, err = xmlquery.ParseWithOptions(strings.NewReader(`&unknown;`), options) + fmt.Println(err != nil) + _, err = xmlquery.ParseWithOptions(strings.NewReader(``), options) + fmt.Println(err != nil) + + options.Decoder.CharsetReader = charset.NewReaderLabel + latin1 := "\xe4 ä" + doc, err = xmlquery.ParseWithOptions(strings.NewReader(latin1), options) + if err != nil { + panic(err) + } + fmt.Println(xmlquery.FindOne(doc, "//name").InnerText()) + + // Output: + // ä & ä + // ä + // ä + // ä + // true + // true + // true + // ä ä +}