diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 6a8c8a1..a8483d3 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -2,8 +2,8 @@
"version": 1,
"isRoot": true,
"tools": {
- "fallout.cli": {
- "version": "11.0.18",
+ "fallout.globaltool": {
+ "version": "10.4.0",
"commands": [
"fallout"
]
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f0dba81
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,350 @@
+# AGENTS.md
+
+## Repository Purpose
+
+AngleSharp.Xml is the AngleSharp ecosystem's XML extension library. It provides:
+
+- XML parsing and serialization
+- XML and SVG document integration with AngleSharp BrowsingContext
+- An AngleSharp DOM model for XML documents
+- XML fragments and asynchronous parsing
+- DTD parsing and practical validity checks
+- Namespace-aware XML handling
+
+The package is published as `AngleSharp.Xml` and is MIT licensed. The repository is hosted at `https://github.com/AngleSharp/AngleSharp.Xml`.
+
+The public README describes the intended positioning: XML behavior that integrates with AngleSharp's DOM, configuration, loading, SVG, XHTML, and formatting APIs. Do not assume this project should be made equivalent to `System.Xml`; preserve the AngleSharp integration model.
+
+## Current Checkout Facts
+
+- Primary development branch is `devel`.
+- The repository currently uses the Fallout build orchestrator, version 10.4.x in the checked-in build tooling.
+- The current project version in `src/Directory.Build.props` is `1.2.0`.
+- The package's AngleSharp dependency defaults to version `1.5.0` and allows versions below `2.0.0`.
+- There is no repository-level AGENTS.md or copilot instruction file other than this one.
+- The worktree should be checked with `git status --short` before making assumptions about local changes. Never discard changes that are already present.
+
+## Important Commands
+
+### Fast local test
+
+From the repository root:
+
+```sh
+dotnet test src/AngleSharp.Xml.Tests/AngleSharp.Xml.Tests.csproj -v q
+```
+
+The tests target `net8.0`. The test project references the library with its `netstandard2.0` target framework, so a test-only run does not necessarily exercise every library target framework.
+
+Run one fixture or test with NUnit adapter filtering:
+
+```sh
+dotnet test src/AngleSharp.Xml.Tests/AngleSharp.Xml.Tests.csproj \
+ --filter 'FullyQualifiedName~AngleSharp.Xml.Tests.Parser.XmlInvalidDocuments'
+```
+
+Use a more specific filter for a single method when debugging. Avoid broad test output when possible; filter or redirect it and inspect the final `Test summary`.
+
+### Build orchestrator
+
+The supported repository build entry points are:
+
+```sh
+./build.sh
+```
+
+```powershell
+.\build.ps1
+```
+
+The scripts:
+
+1. Prefer a globally installed `dotnet`.
+2. Otherwise install a local SDK under `.fallout/temp`.
+3. Restore local .NET tools.
+4. Invoke the Fallout build target.
+
+The default local target runs restore, compile, tests, and package creation. The build reads the version from `CHANGELOG.md` and supports parameters such as:
+
+```sh
+./build.sh -AngleSharpVersion 1.5.0
+./build.sh -Target RunUnitTests
+./build.sh -Target Compile
+./build.sh -Target Package
+```
+
+The exact target names are defined in `build/Build.cs`. Important targets include:
+
+- `Clean`
+- `Restore`
+- `Compile`
+- `RunUnitTests`
+- `CreatePackage`
+- `Package`
+- `Publish`
+- `PrePublish`
+
+Publishing requires secrets and must not be attempted casually:
+
+- `NUGET_API_KEY` for NuGet publishing
+- `GITHUB_TOKEN` for GitHub release publishing
+
+### Direct project commands
+
+Useful focused commands from the root:
+
+```sh
+dotnet build src/AngleSharp.Xml/AngleSharp.Xml.csproj
+dotnet build src/AngleSharp.Xml.Tests/AngleSharp.Xml.Tests.csproj
+dotnet test src/AngleSharp.Xml.Tests/AngleSharp.Xml.Tests.csproj
+```
+
+The solution is `src/AngleSharp.Xml.sln`; it contains the library, tests, and build project. CI uses the repository build scripts rather than direct solution commands.
+
+## CI and Target Frameworks
+
+`.github/workflows/ci.yml` runs on push and pull request.
+
+- Linux uses .NET `10.0.x` and runs `./build.sh -AngleSharpVersion 1.5.0`.
+- Windows uses .NET `10.0.x` and runs `build.ps1`.
+- Windows publishes on `main`, pre-publishes on `devel`, and performs the default build for other refs.
+- Documentation deployment is conditional on repository secrets and the configured docs branch/path.
+
+`src/AngleSharp.Xml/AngleSharp.Xml.csproj` targets:
+
+- `netstandard2.0`
+- `net8.0`
+- `net10.0`
+- `net462` and `net472` on Windows only
+
+The project enables XML documentation generation, strong-name signing, SourceLink, symbols, and package metadata. Warnings are treated as errors through `src/Directory.Build.props`.
+
+## Repository Layout
+
+### Root
+
+- `README.md`: package overview and basic XML setup.
+- `CHANGELOG.md`: release notes and source of the build version.
+- `CONTRIBUTORS.md`: contributor information.
+- `LICENSE`: MIT license.
+- `build.sh`, `build.ps1`, `build.cmd`: cross-platform build entry points.
+- `build/`: Fallout build project and build targets.
+- `docs/`: Markdown documentation.
+- `src/`: solution, library, tests, and docs web project.
+
+### Library: `src/AngleSharp.Xml`
+
+Public integration and formatter files:
+
+- `XmlConfigurationExtensions.cs`: `IConfiguration.WithXml()` registration for XML, application/xml, and SVG document factories plus `IXmlParser`.
+- `DomImplementationExtension.cs`: XML DOM factory helpers.
+- `XmlMarkupFormatter.cs`: XML serialization formatter.
+- `AutoSelectedMarkupFormatter.cs` and `MarkupFormatterExtensions.cs`: formatter selection/helpers.
+- `XmlEntityProvider.cs`: built-in XML entities (`amp;`, `lt;`, `gt;`, `apos;`, `quot;`).
+
+DOM files:
+
+- `Dom/IXmlDocument.cs`: public `IXmlDocument`, including `IsValid`.
+- `Dom/ISvgDocument.cs`: SVG document contract.
+- `Dom/Internal/XmlDocument.cs`: internal XML document implementation and validity storage.
+- `Dom/Internal/XmlElement.cs`: XML element implementation.
+- `Dom/Internal/SvgDocument.cs`: SVG document implementation.
+- `Dom/Events/XmlParseEvent.cs`: parser lifecycle event payload.
+
+Parser files:
+
+- `Parser/IXmlParser.cs`: public parser contract for strings, streams, fragments, and async operations.
+- `Parser/XmlParser.cs`: parser facade and document creation.
+- `Parser/XmlDomBuilder.cs`: tree construction, DOCTYPE handling, DTD validation, and entity expansion.
+- `Parser/XmlTokenizer.cs`: XML lexical tokenizer and character/entity reference handling.
+- `Parser/XmlParserOptions.cs`: suppress-errors, source-reference, and element-created callback options.
+- `Parser/XmlParseError*.cs`: parse error definitions and exception mapping.
+- `Parser/Tokens/`: token models for declarations, DOCTYPE, tags, text, CDATA, comments, processing instructions, and EOF.
+
+DTD files:
+
+- `Dtd/Parser/DtdParser.cs`: DTD parser.
+- `Dtd/Parser/DtdTokenizer.cs`: DTD lexical/token parsing.
+- `Dtd/Parser/DtdPlainTokenizer.cs`: plain external DTD tokenization support.
+- `Dtd/Parser/DtdContainer.cs`: parsed DTD declarations/entities and invalid state.
+- `Dtd/Declaration/`: element and attribute declaration models.
+
+### Tests: `src/AngleSharp.Xml.Tests`
+
+- `Parser/XmlParsing.cs`: general parser behavior.
+- `Parser/XmlNamespace.cs`: namespace behavior.
+- `Parser/XmlValidDocuments.cs`: large valid XML conformance fixture suite.
+- `Parser/XmlInvalidDocuments.cs`: invalid document/DTD fixture suite.
+- `Parser/XmlValidExtDtd.cs`: valid external DTD fixtures.
+- `Parser/XmlNotWfDocuments.cs`: not-well-formed XML fixtures.
+- `Parser/XmlNotWfExtDtd.cs`: not-well-formed external DTD fixtures.
+- `Parser/XmlDtdImplementedCases.cs`: focused, currently supported DTD validation cases.
+- `Parser/XmlExternalDtdSupport.cs`: local-file external DTD/entity regression tests.
+- `Tokenizer/XmlDTD.cs`: DTD tokenizer/parser samples, including historical commented assertions.
+- `Tokenizer/XmlTokenization.cs`: XML tokenizer tests.
+- `Dom/`: DOM samples and tree behavior.
+- `Xhtml/`: XHTML formatter and preservation tests.
+- `Mocks/MockEntityProvider.cs`: entity provider test double.
+- `TestExtensions.cs`: test parsing helpers, including permissive conformance fallback behavior.
+
+The tests use NUnit 3, NUnit3TestAdapter, Microsoft.NET.Test.Sdk, and target `net8.0`.
+
+### Documentation
+
+Markdown documentation is under `docs/`:
+
+- `docs/general/01-Basics.md`: setup and NuGet usage.
+- `docs/general/02-Capabilities.md`: supported capabilities.
+- `docs/general/03-Limitations.md`: project boundaries.
+- `docs/tutorials/01-API.md`: parser/configuration APIs.
+- `docs/tutorials/02-Examples.md`: code examples.
+- `docs/tutorials/03-Use-Cases.md`: practical workflows.
+- `docs/tutorials/04-Questions.md`: FAQ.
+- `docs/tutorials/05-DTD-Validation.md`: DTD concepts, validation, supported behavior, and limitations.
+
+The docs web project is under `src/AngleSharp.Xml.Docs` and uses Node.js/npm with a TypeScript/React entry point. Its CI deployment is secret-controlled; ordinary library changes generally only need Markdown updates unless the docs web app itself is changed.
+
+## Public Usage Patterns
+
+### Register XML in AngleSharp
+
+```cs
+var config = Configuration.Default
+ .WithXml();
+```
+
+`WithXml()` registers document factories for:
+
+- `text/xml`
+- `application/xml`
+- SVG content
+
+It also registers an `IXmlParser` service in the browsing context.
+
+### Parse directly
+
+```cs
+var parser = new XmlParser();
+var document = parser.ParseDocument(xmlText);
+```
+
+The parser also supports streams, fragments, and asynchronous overloads. `XmlParserExtensions` supplies cancellation-token-free async convenience methods.
+
+### Parser options
+
+- `IsSuppressingErrors`: recovery mode; the parser attempts to return a document instead of throwing. The resulting DOM may be incomplete or surprising. Do not treat this as strict validation.
+- `IsKeepingSourceReferences`: keeps source token references on created elements for diagnostics/tooling.
+- `OnCreated`: callback receiving each created element and its `TextPosition`.
+
+### Validity
+
+`IXmlDocument.IsValid` is the library's DTD-related validity signal. It is initialized true and is updated after parsing when applicable. A document can be syntactically parsed but invalid according to its DOCTYPE/DTD declarations.
+
+## DTD Behavior and Boundaries
+
+The DTD implementation is useful but intentionally should not be treated as a complete XML 1.0 validation stack.
+
+Currently supported or partially supported behavior includes:
+
+- DOCTYPE root-name consistency checking.
+- Internal subset declarations for common validation cases.
+- Common element models such as `ANY`, `EMPTY`, mixed `(#PCDATA|name|...)*`, and simple ordered sequences.
+- Internal attribute checks, including undeclared attributes, `#REQUIRED`, and `#FIXED` cases.
+- Internal general entity replacement.
+- Local file-based external `SYSTEM` subset loading.
+- Local external general entity replacement in supported cases.
+
+Known limitations:
+
+- No HTTP/network retrieval for external DTDs/entities.
+- No complete PUBLIC identifier or XML catalog resolution workflow.
+- Parameter entities and external-subset semantics are not full XML conformance.
+- Complex content-model grammar and all quantifier combinations are not guaranteed by fallback validation paths.
+- DTD default-value materialization is limited.
+- No built-in XSD validation.
+- Recovery/conformance test helpers can mask parser gaps and must not be used as evidence of strict parsing.
+
+When changing DTD code, update both focused tests in `XmlDtdImplementedCases.cs`/`XmlExternalDtdSupport.cs` and the DTD documentation if the support boundary changes.
+
+## Parser Change Guidance
+
+The main ownership path for XML behavior is:
+
+1. `XmlTokenizer` recognizes lexical XML constructs.
+2. `XmlDomBuilder` consumes tokens and controls tree state.
+3. `XmlDomBuilder.ApplyValidation()` sets `XmlDocument.IsValid` after the tree is built.
+4. `XmlParser` exposes the builder through public synchronous/asynchronous APIs.
+
+When debugging a behavior:
+
+- If parsing throws before a DOM exists, inspect `XmlTokenizer` and `XmlParseError` first.
+- If the DOM shape is wrong, inspect `XmlDomBuilder` state transitions and token consumption.
+- If `IsValid` is wrong, inspect DOCTYPE parsing, DTD declarations, `ApplyValidation`, and recursive content/attribute checks.
+- If an entity is unresolved, inspect both tokenizer entity lookup and DTD declaration loading.
+- If loading through a browsing context fails, inspect `XmlConfigurationExtensions` and document factory registration.
+
+Prefer the smallest change at the controlling layer. Keep strict parsing and recovery parsing separate; do not make production parser behavior permissive just to satisfy a broad conformance fixture.
+
+## Testing Guidance
+
+Use focused tests first, then the full suite:
+
+```sh
+dotnet test src/AngleSharp.Xml.Tests/AngleSharp.Xml.Tests.csproj \
+ --filter 'FullyQualifiedName~XmlDtdImplementedCases'
+
+dotnet test src/AngleSharp.Xml.Tests/AngleSharp.Xml.Tests.csproj \
+ --filter 'FullyQualifiedName~XmlExternalDtdSupport'
+
+dotnet test src/AngleSharp.Xml.Tests/AngleSharp.Xml.Tests.csproj -v q
+```
+
+For parser changes, include tests for:
+
+- Strict well-formed input.
+- Strict malformed input that must throw.
+- Valid DTD input and invalid DTD input through `IsValid`.
+- Internal and external entity behavior where relevant.
+- Recovery mode only when recovery behavior is explicitly the subject.
+
+Conformance fixture suites are broad and include historical XML test cases that depend on external resources, complex DTD features, or behavior not fully implemented in this repository. Treat `ToXmlDocumentConformance` and similar helpers as compatibility/recovery harnesses, not as a replacement for strict parser assertions.
+
+## Coding and Editing Conventions
+
+- C# uses four spaces; project files use two spaces.
+- Repository line endings are LF and files use UTF-8.
+- Warnings are errors for the library build.
+- Preserve existing public APIs and AngleSharp patterns.
+- Avoid unrelated formatting or refactoring.
+- Use explicit, descriptive variable names; follow the surrounding style.
+- Keep comments concise and explanatory only where needed.
+- Do not add license headers.
+- Do not commit, reset, checkout, or create branches unless explicitly requested.
+- Do not remove user changes from a dirty worktree.
+- Keep generated `bin/`, `obj/`, `.fallout/`, and test-result artifacts out of source changes unless the task explicitly concerns them.
+
+## Common Pitfalls
+
+- The test project uses `netstandard2.0` for its library project reference even though the tests run on `net8.0`.
+- Windows-only target frameworks are conditionally added based on `OS == Windows_NT`; do not assume Linux can build `net462`/`net472`.
+- The build version comes from `CHANGELOG.md`, not only the csproj version property.
+- `WithXml()` is required for BrowsingContext XML/SVG loading; direct `XmlParser` construction is separate.
+- XML entity names passed to the built-in provider include the trailing semicolon (`amp;`, not `amp`).
+- `IsSuppressingErrors` changes control flow and can produce a non-null but structurally unreliable document.
+- A passing broad conformance helper test does not prove strict XML conformance.
+- External DTD support is local-file-oriented and must not be described as network/catalog-aware.
+- DTD `IsValid` is not XSD validation and does not replace domain-specific validation.
+- The docs index and DTD support matrix should be updated when DTD behavior changes.
+
+## Preferred Change Workflow
+
+1. Check `git status --short` and identify the nearest file/symbol/test.
+2. Read the controlling code path and one neighboring test.
+3. State a falsifiable local hypothesis and choose the cheapest focused test.
+4. Make the smallest edit with the repository's existing style.
+5. Run the focused test immediately.
+6. Repair only the same local slice if it fails; do not broaden prematurely.
+7. Run the relevant fixture or project test suite.
+8. Run the full test suite when shared parser, tokenizer, DOM, DTD, or build behavior changed.
+9. Update docs and tests when public behavior or support boundaries change.
+10. Report exact validation commands and any remaining limitations.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68b06de..19a3f01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+# 1.2.0
+
+Released on Friday, August 21 2026.
+
+- Updated to complete DTD identity and reference validation semantics (#31)
+- Improved preservation of CDATA sections as first-class DOM nodes (#30)
+- Added the XML-specific `CDATA` factory (#29)
+- Added support for XML document metadata (#29)
+- Added Canonical XML 1.1 and Exclusive XML Canonicalization 1.0 serialization (#34)
+- Added XML Base, `xml:id`, and inherited `xml:lang` convenience semantics (#33)
+- Added optional XSD 1.0 document validation with diagnostics (#35)
+
# 1.1.0
Released on Friday, July 31 2026.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..9f02784
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,8 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+The guidance is shared with every AI agent working here, so it lives in AGENTS.md and is
+imported below. Record new guidance there rather than in this file.
+
+@AGENTS.md
diff --git a/README.md b/README.md
index 68b2196..a34832a 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,6 @@
[](https://github.com/AngleSharp/AngleSharp.Xml/releases)
[](https://www.nuget.org/packages/AngleSharp.Xml/)
[](https://github.com/AngleSharp/AngleSharp.Xml/issues)
-[](https://gitter.im/AngleSharp/AngleSharp)
-[](https://stackoverflow.com/tags/anglesharp)
[](https://cla-assistant.io/AngleSharp/AngleSharp.Xml)
AngleSharp.Xml extends the core AngleSharp library with some XML capabilities. This repository is the home of the source for the AngleSharp.Xml NuGet package.
diff --git a/build/_build.csproj b/build/_build.csproj
index 647ac26..11c89b0 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -11,9 +11,7 @@
-
-
-
+
diff --git a/docs/general/02-Capabilities.md b/docs/general/02-Capabilities.md
index caaa7ac..9e68beb 100644
--- a/docs/general/02-Capabilities.md
+++ b/docs/general/02-Capabilities.md
@@ -18,6 +18,11 @@ AngleSharp.Xml extends the AngleSharp ecosystem with XML-native parsing and seri
- Use AngleSharp DOM interfaces (IDocument, IElement, IAttr, INode)
- Query and update XML nodes with the same API style used in AngleSharp
- Manipulate attributes, text nodes, comments, and processing instructions
+- Inspect XML declaration version, encoding, and standalone metadata
+- Create and preserve XML CDATA section nodes
+- Resolve effective XML Base URIs and URLs
+- Normalize and look up live `xml:id` values
+- Resolve inherited `xml:lang` values
## Namespace handling
@@ -29,12 +34,17 @@ AngleSharp.Xml extends the AngleSharp ecosystem with XML-native parsing and seri
- Produces XML documents and SVG documents depending on content type
- Works with XML-oriented workflows in mixed markup processing pipelines
+- Validates DTD ID uniqueness and IDREF / IDREFS references
+- Validates ENTITY / ENTITIES against declared unparsed entities and notations
+- Looks up elements by their DTD-declared ID attributes
## Serialization
- Serialize to XML-oriented output with ToXml
- Use auto-selected formatter behavior with ToMarkup
- Configure empty-element behavior using XmlMarkupFormatter.IsAlwaysSelfClosing
+- Serialize documents and rooted element subtrees using Canonical XML 1.1
+- Serialize using Exclusive XML Canonicalization 1.0 with inclusive namespace prefixes
## Diagnostics and control
@@ -42,6 +52,9 @@ AngleSharp.Xml extends the AngleSharp ecosystem with XML-native parsing and seri
- Keep source references for analysis or tooling
- Observe element creation positions via callback hooks
- Subscribe to parser lifecycle events (Parsing, Parsed, Error)
+- Validate existing documents against one or more XSD 1.0 schemas
+- Collect XSD errors and warnings with available source locations
+- Resolve trusted schema imports and includes through a configured resolver
## Typical high-value scenarios
diff --git a/docs/general/03-Limitations.md b/docs/general/03-Limitations.md
index d17dd59..4039f0c 100644
--- a/docs/general/03-Limitations.md
+++ b/docs/general/03-Limitations.md
@@ -6,14 +6,20 @@ section: "AngleSharp.Xml"
AngleSharp.Xml is designed for practical XML parsing and DOM workflows in the AngleSharp ecosystem. It is not intended to replace every specialized XML stack.
-## Not a full XML schema stack
+## XSD validation scope
-AngleSharp.Xml does not provide a full XSD validation subsystem. If strict schema validation is required, pair it with dedicated validation tools.
+XSD validation targets XML Schema 1.0 through the platform `System.Xml.Schema` engine. XML Schema 1.1 is not supported. Validation operates on the current serialized DOM after parsing, so diagnostic line positions describe that representation rather than necessarily matching the original source after DOM mutations.
+
+External schema imports and includes are disabled by default. A configured resolver should only be enabled for trusted schema locations.
## Query model differences
AngleSharp.Xml is centered on AngleSharp DOM operations and selector-based querying. If your architecture requires XPath-first querying, plan for an additional library.
+## Entity reference nodes
+
+AngleSharp's core DOM does not expose entity reference nodes. `IXmlDocument.CreateEntityReference` explicitly throws `NotSupportedException`; entity references encountered while parsing are resolved to replacement text instead.
+
## Error suppression tradeoff
When IsSuppressingErrors is enabled, malformed input may still produce a DOM, but document structure can be surprising. Treat this as recovery mode, not strict validation mode.
@@ -22,6 +28,10 @@ When IsSuppressingErrors is enabled, malformed input may still produce a DOM, bu
Serialization behavior depends on the selected formatter. If deterministic output style is important, explicitly choose XmlMarkupFormatter and configure it instead of relying on auto-selection.
+## Canonical XML input scope
+
+Canonical serialization operates on the existing AngleSharp DOM and accepts complete documents or rooted element subtrees. It does not accept arbitrary XPath node sets. Canonical output therefore reflects the declarations, entity replacements, and default attributes materialized by the parser; the current partial DTD implementation may not materialize every default required by a validating XML processor.
+
## Performance and memory
Like other DOM parsers, full-document parsing keeps an in-memory object graph. For very large inputs, consider chunking or stream-first preprocessing before constructing a full DOM.
@@ -40,6 +50,6 @@ Use AngleSharp.Xml when you want:
Use additional tooling when you need:
-- Strict schema validation
+- XML Schema 1.1 validation
- XPath-centric querying
- Specialized industry-specific XML validation stacks
diff --git a/docs/tutorials/01-API.md b/docs/tutorials/01-API.md
index 04a7463..dde41a2 100644
--- a/docs/tutorials/01-API.md
+++ b/docs/tutorials/01-API.md
@@ -110,6 +110,76 @@ var formatter = new XmlMarkupFormatter
var xml = document.ToHtml(formatter);
```
+### Canonical XML
+
+`ToCanonicalXml` produces canonical UTF-8 bytes without a byte-order mark. Canonical XML 1.1 is the default mode.
+
+```cs
+var canonicalBytes = document.ToCanonicalXml();
+```
+
+Select Exclusive XML Canonicalization 1.0 and its inclusive namespace prefixes through options:
+
+```cs
+var options = new XmlCanonicalizationOptions
+{
+ Mode = XmlCanonicalizationMode.ExclusiveXml10,
+ IncludeComments = true,
+ InclusiveNamespacePrefixes = new[] { "ds", "#default" },
+};
+
+document.ToCanonicalXml(outputStream, options);
+```
+
+The stream overload leaves the destination stream open. Both modes remove XML declarations and doctypes, expand empty elements, replace CDATA boundaries with character content, normalize escaping, and order namespace declarations and attributes canonically.
+
+Canonicalization accepts complete documents and rooted element subtrees. Canonical XML 1.1 subtree output carries applicable ancestor namespace, `xml:lang`, `xml:space`, and fixed-up `xml:base` context. Exclusive mode emits visibly used namespaces plus any configured inclusive prefixes.
+
+### XSD validation
+
+Validate an existing document against one or more inline XML Schema 1.0 documents:
+
+```cs
+var result = document.ValidateXsd(commonSchema, documentSchema);
+
+if (!result.IsValid)
+{
+ foreach (var diagnostic in result.Diagnostics)
+ {
+ Console.WriteLine($"{diagnostic.Severity}: {diagnostic.Message} ({diagnostic.LineNumber}:{diagnostic.LinePosition})");
+ }
+}
+```
+
+Validation collects errors and warnings by default. Use options to stop after the first error or suppress warnings:
+
+```cs
+var options = new XsdValidationOptions
+{
+ IsFailFast = true,
+ IsReportingWarnings = false,
+};
+
+var result = document.ValidateXsd(schemas, options);
+```
+
+For schemas using `xs:include` or `xs:import` with locations, configure a `System.Xml.Schema.XmlSchemaSet` with source URIs and a resolver, then pass it to `ValidateXsd`. External resolution is disabled by default; enable a resolver only for trusted schema locations.
+
+```cs
+var schemas = new XmlSchemaSet
+{
+ XmlResolver = new XmlUrlResolver(),
+};
+schemas.Add(null, schemaPath);
+
+var result = document.ValidateXsd(schemas, new XsdValidationOptions
+{
+ SchemaResolver = new XmlUrlResolver(),
+});
+```
+
+Diagnostic locations refer to the current serialized DOM used for post-parse validation. A source URI is included when the document or configured schema provides one.
+
## DOM model and querying
AngleSharp.Xml uses AngleSharp DOM interfaces and works with standard operations:
@@ -126,6 +196,52 @@ var item = document.QuerySelector("item");
item.SetAttribute("status", "active");
```
+### XML namespace semantics
+
+Common attributes from the XML namespace have convenience APIs on elements and documents.
+
+```cs
+var item = document.QuerySelector("item");
+
+var effectiveBaseUri = item.GetXmlBaseUri();
+var effectiveBaseUrl = item.GetXmlBaseUrl();
+var effectiveLanguage = item.GetXmlLanguage();
+var xmlId = item.GetXmlId();
+var target = document.GetElementByXmlId("chapter-1");
+```
+
+`GetXmlBaseUri` resolves inherited `xml:base` values against the document URL and returns non-ASCII LEIRI characters without escaping. `GetXmlBaseUrl` returns AngleSharp's URL representation, whose `Href` is URI-escaped.
+
+`GetXmlLanguage` returns the nearest inherited `xml:lang` value. An empty value resets inherited language information and is returned as an empty string; null means no language was declared.
+
+Parsed `xml:id` values receive ID whitespace normalization. `GetElementByXmlId` searches current DOM state in document order, so attribute mutations are reflected immediately.
+
+### XML declaration metadata
+
+`IXmlDocument` exposes the parsed XML declaration. Documents without a declaration use XML 1.0 defaults and have a null `XmlEncoding`.
+
+```cs
+var document = parser.ParseDocument(
+ "");
+
+Console.WriteLine(document.XmlVersion); // 1.0
+Console.WriteLine(document.XmlEncoding); // utf-8
+Console.WriteLine(document.XmlStandalone); // true
+```
+
+### CDATA sections
+
+Create XML-native CDATA sections through `IXmlDocument`. Parsed CDATA sections are also preserved as `IXmlCDataSection` nodes during DOM transformations and XML serialization.
+
+```cs
+var section = document.CreateCDataSection("content");
+document.DocumentElement.AppendChild(section);
+```
+
+CDATA content cannot contain the closing delimiter `]]>`. Creation and character-data mutations that would introduce it throw `DomException` without changing the section.
+
+AngleSharp's core DOM does not expose entity reference nodes. `CreateEntityReference` therefore throws `NotSupportedException`; parsed entity references continue to be resolved to their replacement text.
+
## DTD validity signal
When a document contains DOCTYPE declarations, AngleSharp.Xml evaluates DTD-related validity and exposes the result via `document.IsValid`.
diff --git a/docs/tutorials/05-DTD-Validation.md b/docs/tutorials/05-DTD-Validation.md
index 8f8378b..1f72d95 100644
--- a/docs/tutorials/05-DTD-Validation.md
+++ b/docs/tutorials/05-DTD-Validation.md
@@ -75,12 +75,28 @@ Current DTD-related behavior includes:
- Undeclared attributes are flagged invalid (except namespace declarations)
- #REQUIRED constraints are enforced
- #FIXED constraints are enforced when attribute is present
+ - ID values are normalized and must be unique across the document
+ - Each element type may declare at most one ID attribute, with #IMPLIED or #REQUIRED defaults
+ - IDREF and IDREFS values must resolve to declared IDs, including forward references
+ - ENTITY and ENTITIES values must name declared unparsed entities
+ - Unparsed entities and NOTATION attributes must reference declared notations
- Internal general entity replacement in text nodes for declared internal entities
- External subset loading for local file-based SYSTEM identifiers
- Absolute file paths are supported
- Relative paths are resolved against the current process working directory
- External general entity replacement when entities are declared in loaded local external subsets
+## DTD ID lookup
+
+DTD-declared IDs can be inspected and looked up after parsing:
+
+```cs
+var id = element.GetDtdId();
+var target = document.GetElementByDtdId("chapter-1");
+```
+
+Lookup reads the current attribute value, so later DOM mutations are reflected immediately. The declared ID attribute metadata is preserved when XML elements are cloned.
+
## What is currently limited or not supported
You should be aware of these boundaries:
@@ -92,7 +108,7 @@ You should be aware of these boundaries:
- Full content-model grammar support is incomplete in internal fallback paths
- Complex nested groups and advanced quantifier combinations may not be fully validated
- Attribute default-value materialization from DTD declarations is limited
-- XSD validation is not included
+- XML Schema 1.0 validation is available separately through `ValidateXsd`
## Recommended usage pattern
diff --git a/src/AngleSharp.Xml.Docs/package.json b/src/AngleSharp.Xml.Docs/package.json
index 98a161a..32f41fe 100644
--- a/src/AngleSharp.Xml.Docs/package.json
+++ b/src/AngleSharp.Xml.Docs/package.json
@@ -1,6 +1,6 @@
{
"name": "@anglesharp/xml",
- "version": "1.1.0",
+ "version": "1.2.0",
"preview": true,
"description": "The doclet for the AngleSharp.Xml documentation.",
"keywords": [
diff --git a/src/AngleSharp.Xml.Tests/Canonicalization/XmlCanonicalization.cs b/src/AngleSharp.Xml.Tests/Canonicalization/XmlCanonicalization.cs
new file mode 100644
index 0000000..8e17e7b
--- /dev/null
+++ b/src/AngleSharp.Xml.Tests/Canonicalization/XmlCanonicalization.cs
@@ -0,0 +1,127 @@
+namespace AngleSharp.Xml.Tests.Canonicalization
+{
+ using AngleSharp.Dom;
+ using AngleSharp.Io;
+ using NUnit.Framework;
+ using System;
+ using System.IO;
+ using System.Text;
+ using System.Threading.Tasks;
+
+ [TestFixture]
+ public class XmlCanonicalization
+ {
+ [Test]
+ public void CanonicalXml11NormalizesCoreSyntax()
+ {
+ var document = @"
+
+0 && value<10]]>".ToXmlDocument();
+
+ var result = Encoding.UTF8.GetString(document.ToCanonicalXml());
+
+ Assert.AreEqual("value>0 && value<10", result);
+ }
+
+ [Test]
+ public void CanonicalXml11OrdersNamespacesAndAttributes()
+ {
+ var document = @"".ToXmlDocument();
+
+ var result = Encoding.UTF8.GetString(document.ToCanonicalXml());
+
+ Assert.AreEqual("", result);
+ }
+
+ [Test]
+ public void CanonicalXml11SubtreeIncludesAncestorContext()
+ {
+ var document = @"".ToXmlDocument();
+ var item = document.QuerySelector("item");
+
+ var result = Encoding.UTF8.GetString(item.ToCanonicalXml());
+
+ Assert.AreEqual("", result);
+ }
+
+ [Test]
+ public void CanonicalXml11ResolvesOmittedAncestorXmlBases()
+ {
+ var document = @"".ToXmlDocument();
+ var element = document.QuerySelector("d");
+
+ var result = Encoding.UTF8.GetString(element.ToCanonicalXml());
+
+ Assert.AreEqual("", result);
+ }
+
+ [Test]
+ public void ExclusiveXml10OnlyIncludesVisibleAndRequestedNamespaces()
+ {
+ var document = @"".ToXmlDocument();
+ var item = document.QuerySelector("item");
+ var options = new XmlCanonicalizationOptions
+ {
+ Mode = XmlCanonicalizationMode.ExclusiveXml10,
+ InclusiveNamespacePrefixes = new[] { "z" },
+ };
+
+ var result = Encoding.UTF8.GetString(item.ToCanonicalXml(options));
+
+ Assert.AreEqual("", result);
+ }
+
+ [Test]
+ public void CanonicalXml11ControlsCommentsAndTopLevelLineBreaks()
+ {
+ var document = "".ToXmlDocument();
+ var withoutComments = Encoding.UTF8.GetString(document.ToCanonicalXml());
+ var withComments = Encoding.UTF8.GetString(document.ToCanonicalXml(new XmlCanonicalizationOptions { IncludeComments = true }));
+
+ Assert.AreEqual("\n\n", withoutComments);
+ Assert.AreEqual("\n\n\n\n", withComments);
+ }
+
+ [Test]
+ public void CanonicalXml11RejectsRelativeNamespaceUris()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.Throws(() => document.ToCanonicalXml());
+ }
+
+ [Test]
+ public void CanonicalXmlWritesUtf8WithoutClosingStream()
+ {
+ var document = "©".ToXmlDocument();
+ var stream = new MemoryStream();
+
+ document.ToCanonicalXml(stream);
+ stream.WriteByte(0x21);
+
+ Assert.AreEqual("©!", Encoding.UTF8.GetString(stream.ToArray()));
+ }
+
+ [Test]
+ public void CanonicalXmlRejectsUnsupportedNodeRoots()
+ {
+ var document = "text".ToXmlDocument();
+
+ Assert.Throws(() => document.DocumentElement.FirstChild.ToCanonicalXml());
+ }
+
+ [Test]
+ public async Task CanonicalXmlSupportsSvgDocuments()
+ {
+ var document = await BrowsingContext.New(Configuration.Default.WithXml()).OpenAsync(request =>
+ request.Content("")
+ .Header(HeaderNames.ContentType, MimeTypeNames.Svg));
+
+ var result = Encoding.UTF8.GetString(document.ToCanonicalXml());
+
+ Assert.AreEqual("", result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml.Tests/Dom/XmlAttributeSemantics.cs b/src/AngleSharp.Xml.Tests/Dom/XmlAttributeSemantics.cs
new file mode 100644
index 0000000..d37c0f5
--- /dev/null
+++ b/src/AngleSharp.Xml.Tests/Dom/XmlAttributeSemantics.cs
@@ -0,0 +1,88 @@
+namespace AngleSharp.Xml.Tests.Dom
+{
+ using AngleSharp.Io;
+ using NUnit.Framework;
+ using System;
+ using System.Threading.Tasks;
+
+ [TestFixture]
+ public class XmlAttributeSemantics
+ {
+ [Test]
+ public void XmlBaseResolvesAcrossAncestorChain()
+ {
+ var document = " ".ToXmlDocument();
+ var item = document.QuerySelector("item");
+
+ Assert.AreEqual("https://example.com/a/assets/", item.GetXmlBaseUri());
+ Assert.AreEqual("https://example.com/a/assets/", item.GetXmlBaseUrl().Href);
+ }
+
+ [Test]
+ public void XmlBasePreservesNonAsciiCharactersAndSameDocumentReferences()
+ {
+ var document = " ".ToXmlDocument();
+ var item = document.QuerySelector("item");
+ var child = document.QuerySelector("child");
+
+ Assert.AreEqual("http://example.org/wine/cellar%20one/rosé/%C3%A9", item.GetXmlBaseUri());
+ Assert.AreEqual(item.GetXmlBaseUri(), child.GetXmlBaseUri());
+ }
+
+ [Test]
+ public async Task XmlBaseResolvesAgainstDocumentUrl()
+ {
+ var document = await BrowsingContext.New(Configuration.Default.WithXml()).OpenAsync(request =>
+ request.Address("https://example.com/documents/source.xml")
+ .Content(" ")
+ .Header(HeaderNames.ContentType, MimeTypeNames.Xml));
+
+ Assert.AreEqual("https://example.com/assets/", document.QuerySelector("item").GetXmlBaseUri());
+ }
+
+ [Test]
+ public void XmlLanguageUsesNearestDeclarationAndSupportsReset()
+ {
+ var document = " ".ToXmlDocument();
+ var items = document.QuerySelectorAll("item");
+
+ Assert.AreEqual("en", items[0].GetXmlLanguage());
+ Assert.AreEqual("fr", items[1].GetXmlLanguage());
+ Assert.AreEqual(String.Empty, items[2].GetXmlLanguage());
+ }
+
+ [Test]
+ public void XmlIdIsNormalizedAndFoundInDocumentOrder()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.AreEqual("item one", document.DocumentElement.FirstElementChild.Attributes["xml:id"].Value);
+ Assert.AreEqual("item one", document.DocumentElement.FirstElementChild.GetXmlId());
+ Assert.AreSame(document.DocumentElement.FirstElementChild, document.GetElementByXmlId("item one"));
+ }
+
+ [Test]
+ public void XmlIdLookupReflectsDomMutations()
+ {
+ var document = " ".ToXmlDocument();
+ var item = document.DocumentElement.FirstElementChild;
+
+ item.Attributes["xml:id"].Value = "after";
+
+ Assert.IsNull(document.GetElementByXmlId("before"));
+ Assert.AreSame(item, document.GetElementByXmlId("after"));
+ }
+
+ [Test]
+ public void XmlConvenienceSemanticsIgnoreSameNamedUnqualifiedAttributes()
+ {
+ var document = " ".ToXmlDocument();
+ var item = document.DocumentElement.FirstElementChild;
+
+ Assert.AreEqual(item.BaseUri, item.GetXmlBaseUri());
+ Assert.IsNull(document.DocumentElement.GetXmlId());
+ Assert.IsNull(item.GetXmlLanguage());
+ Assert.IsNull(document.GetElementByXmlId("wrong"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml.Tests/Dom/XmlDocumentFactories.cs b/src/AngleSharp.Xml.Tests/Dom/XmlDocumentFactories.cs
new file mode 100644
index 0000000..7f93c79
--- /dev/null
+++ b/src/AngleSharp.Xml.Tests/Dom/XmlDocumentFactories.cs
@@ -0,0 +1,136 @@
+namespace AngleSharp.Xml.Tests.Dom
+{
+ using AngleSharp.Dom;
+ using AngleSharp.Xml.Dom;
+ using NUnit.Framework;
+ using System;
+
+ [TestFixture]
+ public class XmlDocumentFactories
+ {
+ [Test]
+ public void DeclarationMetadataIsExposed()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.AreEqual("1.0", document.XmlVersion);
+ Assert.AreEqual("ISO-8859-1", document.XmlEncoding);
+ Assert.IsTrue(document.XmlStandalone);
+ }
+
+ [Test]
+ public void DeclarationMetadataUsesXmlDefaultsWhenOmitted()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.AreEqual("1.0", document.XmlVersion);
+ Assert.IsNull(document.XmlEncoding);
+ Assert.IsFalse(document.XmlStandalone);
+ }
+
+ [Test]
+ public void ClonePreservesDeclarationMetadata()
+ {
+ var document = "".ToXmlDocument();
+ var clone = (IXmlDocument)document.Clone();
+
+ Assert.AreEqual(document.XmlVersion, clone.XmlVersion);
+ Assert.AreEqual(document.XmlEncoding, clone.XmlEncoding);
+ Assert.AreEqual(document.XmlStandalone, clone.XmlStandalone);
+ }
+
+ [Test]
+ public void CreateCDataSectionPreservesLiteralMarkup()
+ {
+ var document = "".ToXmlDocument();
+ var section = document.CreateCDataSection("&value");
+
+ document.DocumentElement.AppendChild(section);
+
+ Assert.AreEqual(NodeType.CharacterData, section.NodeType);
+ Assert.AreEqual("&value]]>", document.ToXml());
+ }
+
+ [Test]
+ public void ParsedCDataRemainsCData()
+ {
+ var document = "]]>".ToXmlDocument();
+
+ Assert.IsInstanceOf(document.DocumentElement.FirstChild);
+ Assert.AreEqual("", document.DocumentElement.TextContent);
+ Assert.AreEqual("]]>", document.ToXml());
+ }
+
+ [Test]
+ public void CDataLexicalBoundariesSurviveRoundTrip()
+ {
+ const String source = "text";
+ var document = source.ToXmlDocument();
+
+ Assert.AreEqual(4, document.DocumentElement.ChildNodes.Length);
+ Assert.IsInstanceOf(document.DocumentElement.ChildNodes[0]);
+ Assert.AreEqual(NodeType.Text, document.DocumentElement.ChildNodes[1].NodeType);
+ Assert.IsInstanceOf(document.DocumentElement.ChildNodes[2]);
+ Assert.IsInstanceOf(document.DocumentElement.ChildNodes[3]);
+ Assert.AreEqual(source, document.ToXml());
+ }
+
+ [Test]
+ public void ClonePreservesCDataSections()
+ {
+ var document = "".ToXmlDocument();
+ var clone = (IXmlDocument)document.Clone();
+
+ Assert.IsInstanceOf(clone.DocumentElement.FirstChild);
+ Assert.AreEqual(document.ToXml(), clone.ToXml());
+ }
+
+ [Test]
+ public void AutoSelectedFormatterPreservesCDataWithoutDoctype()
+ {
+ const String source = "]]>";
+ var document = source.ToXmlDocument();
+
+ Assert.AreEqual(source, document.ToMarkup());
+ Assert.AreEqual("]]>", document.DocumentElement.FirstChild.ToMarkup());
+ }
+
+ [Test]
+ public void CreateCDataSectionRejectsClosingDelimiter()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.Throws(() => document.CreateCDataSection("]]>") );
+ }
+
+ [Test]
+ public void CDataMutationsRejectClosingDelimiterAtomically()
+ {
+ var document = "".ToXmlDocument();
+
+ AssertMutationRejected(document.CreateCDataSection("safe"), section => section.Data = "]]>");
+ AssertMutationRejected(document.CreateCDataSection("safe"), section => section.NodeValue = "]]>");
+ AssertMutationRejected(document.CreateCDataSection("safe"), section => section.TextContent = "]]>");
+ AssertMutationRejected(document.CreateCDataSection("]]"), section => section.Append(">"));
+ AssertMutationRejected(document.CreateCDataSection("]]"), section => section.Insert(2, ">"));
+ AssertMutationRejected(document.CreateCDataSection("]]x>"), section => section.Delete(2, 1));
+ AssertMutationRejected(document.CreateCDataSection("safe"), section => section.Replace(0, 4, "]]>") );
+ }
+
+ [Test]
+ public void CreateEntityReferenceIsExplicitlyUnsupported()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.Throws(() => document.CreateEntityReference("entity"));
+ }
+
+ private static void AssertMutationRejected(IXmlCDataSection section, Action mutation)
+ {
+ var original = section.Data;
+
+ Assert.Throws(() => mutation(section));
+ Assert.AreEqual(original, section.Data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml.Tests/Parser/XmlDtdIdentityValidation.cs b/src/AngleSharp.Xml.Tests/Parser/XmlDtdIdentityValidation.cs
new file mode 100644
index 0000000..b9d4b57
--- /dev/null
+++ b/src/AngleSharp.Xml.Tests/Parser/XmlDtdIdentityValidation.cs
@@ -0,0 +1,127 @@
+namespace AngleSharp.Xml.Tests.Parser
+{
+ using AngleSharp.Xml.Dom;
+ using NUnit.Framework;
+ using System;
+ using System.IO;
+
+ [TestFixture]
+ public class XmlDtdIdentityValidation
+ {
+ [Test]
+ public void UniqueIdsAndResolvedReferencesAreValid()
+ {
+ var document = @"
+
+
+
+
+]> ".ToXmlDocument(validating: true);
+
+ Assert.IsTrue(document.IsValid);
+ Assert.AreEqual("a", document.DocumentElement.FirstElementChild.GetDtdId());
+ Assert.AreSame(document.DocumentElement.FirstElementChild, document.GetElementByDtdId("a"));
+ Assert.AreEqual("a", document.QuerySelector("refs").GetAttribute("one"));
+ Assert.AreEqual("a b", document.QuerySelector("refs").GetAttribute("many"));
+ }
+
+ [Test]
+ public void DtdIdLookupReflectsDomMutations()
+ {
+ var document = CreateIdDocument(" ");
+ var item = document.DocumentElement.FirstElementChild;
+
+ item.Attributes["key"].Value = "after";
+
+ Assert.IsNull(document.GetElementByDtdId("before"));
+ Assert.AreSame(item, document.GetElementByDtdId("after"));
+ }
+
+ [Test]
+ public void DuplicateIdIsInvalid()
+ {
+ var document = CreateIdDocument(" ");
+
+ Assert.IsFalse(document.IsValid);
+ }
+
+ [Test]
+ public void UnresolvedIdRefAndIdRefsAreInvalid()
+ {
+ var single = CreateReferenceDocument("IDREF", "missing");
+ var multiple = CreateReferenceDocument("IDREFS", "known missing");
+
+ Assert.IsFalse(single.IsValid);
+ Assert.IsFalse(multiple.IsValid);
+ }
+
+ [Test]
+ public void MultipleIdDeclarationsAndIdDefaultAreInvalid()
+ {
+ var multiple = @"]>".ToXmlDocument(validating: true);
+ var defaulted = @"]>".ToXmlDocument(validating: true);
+
+ Assert.IsFalse(multiple.IsValid);
+ Assert.IsFalse(defaulted.IsValid);
+ }
+
+ [Test]
+ public void EntityAttributesRequireDeclaredUnparsedEntities()
+ {
+ const string declarations = "";
+ var valid = CreateEntityDocument(declarations, "ENTITY", "logo");
+ var invalidParsed = CreateEntityDocument("", "ENTITY", "logo");
+ var invalidMissing = CreateEntityDocument(declarations, "ENTITIES", "logo missing");
+
+ Assert.IsTrue(valid.IsValid);
+ Assert.IsFalse(invalidParsed.IsValid);
+ Assert.IsFalse(invalidMissing.IsValid);
+ }
+
+ [Test]
+ public void UnparsedEntityAndNotationDeclarationsMustBeConsistent()
+ {
+ var missingNotation = CreateEntityDocument("", "ENTITY", "logo");
+ var invalidNotationAttribute = @"]>".ToXmlDocument(validating: true);
+
+ Assert.IsFalse(missingNotation.IsValid);
+ Assert.IsFalse(invalidNotationAttribute.IsValid);
+ }
+
+ [Test]
+ public void ExternalDtdIdentityDeclarationsAreValidated()
+ {
+ var directory = Path.Combine(Path.GetTempPath(), "anglesharp-xml-dtd-id-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+
+ try
+ {
+ var dtdPath = Path.Combine(directory, "identity.dtd");
+ File.WriteAllText(dtdPath, "");
+ var valid = $"".ToXmlDocument(validating: true);
+ var invalid = $"".ToXmlDocument(validating: true);
+
+ Assert.IsTrue(valid.IsValid);
+ Assert.IsFalse(invalid.IsValid);
+ }
+ finally
+ {
+ Directory.Delete(directory, true);
+ }
+ }
+
+ private static IXmlDocument CreateIdDocument(string children) => $@"
+]>{children}".ToXmlDocument(validating: true);
+
+ private static IXmlDocument CreateReferenceDocument(string type, string value) => $@"
+
+]> ".ToXmlDocument(validating: true);
+
+ private static IXmlDocument CreateEntityDocument(string declarations, string type, string value) => $@"{declarations}
+]>".ToXmlDocument(validating: true);
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml.Tests/Validation/XsdValidation.cs b/src/AngleSharp.Xml.Tests/Validation/XsdValidation.cs
new file mode 100644
index 0000000..b0c469c
--- /dev/null
+++ b/src/AngleSharp.Xml.Tests/Validation/XsdValidation.cs
@@ -0,0 +1,160 @@
+namespace AngleSharp.Xml.Tests.Validation
+{
+ using NUnit.Framework;
+ using System;
+ using System.IO;
+ using System.Linq;
+ using System.Xml;
+ using System.Xml.Schema;
+
+ [TestFixture]
+ public class XsdValidation
+ {
+ private const string Schema = @"
+
+
+
+
+
+
+";
+
+ [Test]
+ public void ExistingDocumentValidatesAgainstInlineSchema()
+ {
+ var document = "XML".ToXmlDocument();
+
+ var result = document.ValidateXsd(Schema);
+
+ Assert.IsTrue(result.IsValid);
+ Assert.IsEmpty(result.Diagnostics);
+ }
+
+ [Test]
+ public void InvalidDocumentReturnsDetailedDiagnostics()
+ {
+ var document = "".ToXmlDocument();
+
+ var result = document.ValidateXsd(Schema);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsTrue(result.Diagnostics.Count >= 2);
+ Assert.IsTrue(result.Diagnostics.All(m => m.Severity == XsdValidationSeverity.Error));
+ Assert.IsTrue(result.Diagnostics.All(m => m.LineNumber > 0));
+ }
+
+ [Test]
+ public void FailFastStopsAfterFirstValidationError()
+ {
+ var document = "".ToXmlDocument();
+ var options = new XsdValidationOptions { IsFailFast = true };
+
+ var result = document.ValidateXsd(new[] { Schema }, options);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.AreEqual(1, result.Diagnostics.Count);
+ }
+
+ [Test]
+ public void ValidationWarningsCanBeCollectedOrSuppressed()
+ {
+ var document = "".ToXmlDocument();
+
+ var withWarnings = document.ValidateXsd(new[] { Schema }, new XsdValidationOptions());
+ var withoutWarnings = document.ValidateXsd(new[] { Schema }, new XsdValidationOptions { IsReportingWarnings = false });
+
+ Assert.IsTrue(withWarnings.IsValid);
+ Assert.IsTrue(withWarnings.Diagnostics.Any(m => m.Severity == XsdValidationSeverity.Warning));
+ Assert.IsTrue(withoutWarnings.IsValid);
+ Assert.IsEmpty(withoutWarnings.Diagnostics);
+ }
+
+ [Test]
+ public void MultipleSchemasSupportNamespaceAwareValidation()
+ {
+ var common = @"";
+ var root = @"";
+ var document = "ABC".ToXmlDocument();
+
+ var result = document.ValidateXsd(new[] { common, root }, null);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [Test]
+ public void InvalidSchemaReturnsCompilationDiagnostic()
+ {
+ var document = "".ToXmlDocument();
+ var invalidSchema = "";
+
+ var result = document.ValidateXsd(invalidSchema);
+
+ Assert.IsFalse(result.IsValid);
+ Assert.IsNotEmpty(result.Diagnostics);
+ }
+
+ [Test]
+ public void MalformedSchemaReturnsParsingDiagnostic()
+ {
+ var document = "".ToXmlDocument();
+
+ var result = document.ValidateXsd("");
+
+ Assert.IsFalse(result.IsValid);
+ Assert.AreEqual(1, result.Diagnostics.Count);
+ Assert.Greater(result.Diagnostics[0].LineNumber, 0);
+ }
+
+ [Test]
+ public void InlineValidationRequiresAtLeastOneSchema()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.Throws(() => document.ValidateXsd(new string[0], null));
+ }
+
+ [Test]
+ public void ExistingDoctypeIsNotReprocessedDuringXsdValidation()
+ {
+ var document = "XML".ToXmlDocument();
+
+ var result = document.ValidateXsd(Schema);
+
+ Assert.IsTrue(result.IsValid);
+ }
+
+ [Test]
+ public void ConfiguredSchemaSetResolvesIncludes()
+ {
+ var directory = Path.Combine(Path.GetTempPath(), "anglesharp-xml-xsd-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+
+ try
+ {
+ var includedPath = Path.Combine(directory, "types.xsd");
+ var rootPath = Path.Combine(directory, "root.xsd");
+ File.WriteAllText(includedPath, "");
+ File.WriteAllText(rootPath, "");
+ var schemas = new XmlSchemaSet { XmlResolver = new XmlUrlResolver() };
+ schemas.Add(null, rootPath);
+ var document = "ABC".ToXmlDocument();
+
+ var result = document.ValidateXsd(schemas);
+
+ Assert.IsTrue(result.IsValid);
+ }
+ finally
+ {
+ Directory.Delete(directory, true);
+ }
+ }
+
+ [Test]
+ public void ConfiguredSchemaSetMustNotBeEmpty()
+ {
+ var document = "".ToXmlDocument();
+
+ Assert.Throws(() => document.ValidateXsd(new XmlSchemaSet()));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/AutoSelectedMarkupFormatter.cs b/src/AngleSharp.Xml/AutoSelectedMarkupFormatter.cs
index 89a2d0c..91527a8 100644
--- a/src/AngleSharp.Xml/AutoSelectedMarkupFormatter.cs
+++ b/src/AngleSharp.Xml/AutoSelectedMarkupFormatter.cs
@@ -2,6 +2,7 @@ namespace AngleSharp.Xml
{
using AngleSharp.Dom;
using AngleSharp.Html;
+ using AngleSharp.Xml.Dom;
using AngleSharp.Xhtml;
using System;
@@ -63,21 +64,21 @@ private IMarkupFormatter ChildFormatter
///
public virtual String OpenTag(IElement element, Boolean selfClosing)
{
- Confirm(element.Owner.Doctype);
+ Confirm(element.Owner);
return ChildFormatter.OpenTag(element, selfClosing);
}
///
public virtual String CloseTag(IElement element, Boolean selfClosing)
{
- Confirm(element.Owner.Doctype);
+ Confirm(element.Owner);
return ChildFormatter.CloseTag(element, selfClosing);
}
///
public virtual String Comment(IComment comment)
{
- Confirm(comment.Owner.Doctype);
+ Confirm(comment.Owner);
return ChildFormatter.Comment(comment);
}
@@ -91,15 +92,23 @@ public virtual String Doctype(IDocumentType doctype)
///
public virtual String Processing(IProcessingInstruction processing)
{
- Confirm(processing.Owner.Doctype);
+ Confirm(processing.Owner);
return ChildFormatter.Processing(processing);
}
///
- public virtual String Text(ICharacterData text) => ChildFormatter.Text(text);
+ public virtual String Text(ICharacterData text)
+ {
+ Confirm(text.Owner);
+ return ChildFormatter.Text(text);
+ }
///
- public virtual String LiteralText(ICharacterData text) => ChildFormatter.LiteralText(text);
+ public virtual String LiteralText(ICharacterData text)
+ {
+ Confirm(text.Owner);
+ return ChildFormatter.LiteralText(text);
+ }
#endregion
@@ -113,6 +122,18 @@ private void Confirm(IDocumentType docType)
}
}
+ private void Confirm(IDocument document)
+ {
+ if (childFormatter == null && document is IXmlDocument)
+ {
+ ChildFormatter = XmlMarkupFormatter.Instance;
+ }
+ else
+ {
+ Confirm(document?.Doctype);
+ }
+ }
+
#endregion
}
}
diff --git a/src/AngleSharp.Xml/Dom/IXmlCDataSection.cs b/src/AngleSharp.Xml/Dom/IXmlCDataSection.cs
new file mode 100644
index 0000000..eef6a37
--- /dev/null
+++ b/src/AngleSharp.Xml/Dom/IXmlCDataSection.cs
@@ -0,0 +1,13 @@
+namespace AngleSharp.Xml.Dom
+{
+ using AngleSharp.Attributes;
+ using AngleSharp.Dom;
+
+ ///
+ /// Represents a CDATA section in an XML document.
+ ///
+ [DomName("CDATASection")]
+ public interface IXmlCDataSection : ICharacterData
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/Dom/IXmlDocument.cs b/src/AngleSharp.Xml/Dom/IXmlDocument.cs
index 203d5db..cf32662 100644
--- a/src/AngleSharp.Xml/Dom/IXmlDocument.cs
+++ b/src/AngleSharp.Xml/Dom/IXmlDocument.cs
@@ -10,9 +10,39 @@ namespace AngleSharp.Xml.Dom
[DomName("XMLDocument")]
public interface IXmlDocument : IDocument
{
+ ///
+ /// Gets the XML declaration version.
+ ///
+ String XmlVersion { get; }
+
+ ///
+ /// Gets the encoding specified by the XML declaration, if any.
+ ///
+ String XmlEncoding { get; }
+
+ ///
+ /// Gets if the XML declaration specifies a standalone document.
+ ///
+ Boolean XmlStandalone { get; }
+
///
/// Gets if the document is actually valid.
///
Boolean IsValid { get; }
+
+ ///
+ /// Creates a CDATA section owned by this document.
+ ///
+ /// The section's character data.
+ /// The created CDATA section.
+ IXmlCDataSection CreateCDataSection(String data);
+
+ ///
+ /// Entity reference nodes are not supported by AngleSharp's DOM.
+ ///
+ /// The entity name.
+ /// This method does not return.
+ /// Always thrown.
+ INode CreateEntityReference(String name);
}
}
diff --git a/src/AngleSharp.Xml/Dom/Internal/XmlCDataSection.cs b/src/AngleSharp.Xml/Dom/Internal/XmlCDataSection.cs
new file mode 100644
index 0000000..be90cdf
--- /dev/null
+++ b/src/AngleSharp.Xml/Dom/Internal/XmlCDataSection.cs
@@ -0,0 +1,122 @@
+namespace AngleSharp.Xml.Dom
+{
+ using AngleSharp.Dom;
+ using System;
+
+ sealed class XmlCDataSection : Node, IXmlCDataSection
+ {
+ private String _data;
+
+ internal XmlCDataSection(Document owner, String data)
+ : base(owner, "#cdata-section", NodeType.CharacterData)
+ {
+ _data = String.Empty;
+ Data = data;
+ }
+
+ public String Data
+ {
+ get => _data;
+ set
+ {
+ value = value ?? String.Empty;
+
+ if (value.Contains("]]>") )
+ {
+ throw new DomException(DomError.InvalidCharacter);
+ }
+
+ _data = value;
+ }
+ }
+
+ public Int32 Length => _data.Length;
+
+ public IElement NextElementSibling
+ {
+ get
+ {
+ var sibling = ((INode)this).NextSibling;
+
+ while (sibling != null && sibling.NodeType != NodeType.Element)
+ {
+ sibling = sibling.NextSibling;
+ }
+
+ return sibling as IElement;
+ }
+ }
+
+ public IElement PreviousElementSibling
+ {
+ get
+ {
+ var sibling = ((INode)this).PreviousSibling;
+
+ while (sibling != null && sibling.NodeType != NodeType.Element)
+ {
+ sibling = sibling.PreviousSibling;
+ }
+
+ return sibling as IElement;
+ }
+ }
+
+ public override String NodeValue
+ {
+ get => _data;
+ set => Data = value;
+ }
+
+ public override String TextContent
+ {
+ get => _data;
+ set => Data = value;
+ }
+
+ public String Substring(Int32 offset, Int32 count) => _data.Substring(offset, Math.Min(count, _data.Length - offset));
+
+ public void Append(String data) => Data = String.Concat(_data, data);
+
+ public void Insert(Int32 offset, String data) => Data = _data.Insert(offset, data ?? String.Empty);
+
+ public void Delete(Int32 offset, Int32 count) => Data = _data.Remove(offset, Math.Min(count, _data.Length - offset));
+
+ public void Replace(Int32 offset, Int32 count, String data)
+ {
+ var updated = _data.Remove(offset, Math.Min(count, _data.Length - offset));
+ Data = updated.Insert(offset, data ?? String.Empty);
+ }
+
+ public void Before(params INode[] nodes)
+ {
+ var parent = ((INode)this).Parent;
+
+ foreach (var node in nodes)
+ {
+ parent?.InsertBefore(node, this);
+ }
+ }
+
+ public void After(params INode[] nodes)
+ {
+ var parent = ((INode)this).Parent;
+ var reference = ((INode)this).NextSibling;
+
+ foreach (var node in nodes)
+ {
+ parent?.InsertBefore(node, reference);
+ }
+ }
+
+ public void Replace(params INode[] nodes)
+ {
+ Before(nodes);
+ Remove();
+ }
+
+ public void Remove() => ((INode)this).Parent?.RemoveChild(this);
+
+ public override Node Clone(Document owner, Boolean deep) => new XmlCDataSection(owner, _data);
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/Dom/Internal/XmlDocument.cs b/src/AngleSharp.Xml/Dom/Internal/XmlDocument.cs
index 4cd81c3..223728f 100644
--- a/src/AngleSharp.Xml/Dom/Internal/XmlDocument.cs
+++ b/src/AngleSharp.Xml/Dom/Internal/XmlDocument.cs
@@ -11,6 +11,9 @@ namespace AngleSharp.Xml.Dom
sealed class XmlDocument : Document, IXmlDocument
{
private Boolean _isValid;
+ private String _xmlVersion;
+ private String _xmlEncoding;
+ private Boolean _xmlStandalone;
#region ctor
@@ -19,6 +22,7 @@ internal XmlDocument(IBrowsingContext context, TextSource source)
{
ContentType = MimeTypeNames.Xml;
_isValid = true;
+ _xmlVersion = "1.0";
}
internal XmlDocument(IBrowsingContext context = null)
@@ -34,6 +38,12 @@ internal XmlDocument(IBrowsingContext context = null)
public override IEntityProvider Entities => Context.GetProvider() ?? XmlEntityProvider.Resolver;
+ public String XmlVersion => _xmlVersion;
+
+ public String XmlEncoding => _xmlEncoding;
+
+ public Boolean XmlStandalone => _xmlStandalone;
+
public Boolean IsValid => _isValid;
#endregion
@@ -42,9 +52,18 @@ internal XmlDocument(IBrowsingContext context = null)
public override Element CreateElementFrom(String name, String prefix, NodeFlags flags = NodeFlags.None) => new XmlElement(this, name, prefix, flags: flags);
+ public IXmlCDataSection CreateCDataSection(String data)
+ {
+ return new XmlCDataSection(this, data);
+ }
+
+ public INode CreateEntityReference(String name) => throw new NotSupportedException("Entity reference nodes are not supported by AngleSharp's DOM.");
+
public override Node Clone(Document owner, Boolean deep)
{
var node = new XmlDocument(Context, new TextSource(Source.Text));
+ node.SetDeclaration(_xmlVersion, _xmlEncoding, _xmlStandalone);
+ node.SetValidity(_isValid);
CloneDocument(node, deep);
return node;
}
@@ -62,6 +81,13 @@ internal void SetValidity(Boolean isValid)
_isValid = isValid;
}
+ internal void SetDeclaration(String version, String encoding, Boolean standalone)
+ {
+ _xmlVersion = version;
+ _xmlEncoding = encoding;
+ _xmlStandalone = standalone;
+ }
+
#endregion
}
}
diff --git a/src/AngleSharp.Xml/Dom/Internal/XmlElement.cs b/src/AngleSharp.Xml/Dom/Internal/XmlElement.cs
index 48deeef..24f0483 100644
--- a/src/AngleSharp.Xml/Dom/Internal/XmlElement.cs
+++ b/src/AngleSharp.Xml/Dom/Internal/XmlElement.cs
@@ -27,6 +27,27 @@ internal String IdAttribute
set;
}
+ public override String TextContent
+ {
+ get
+ {
+ var content = StringBuilderPool.Obtain();
+
+ foreach (var child in ((INode)this).ChildNodes)
+ {
+ if (child.NodeType == NodeType.Element ||
+ child.NodeType == NodeType.Text ||
+ child.NodeType == NodeType.CharacterData)
+ {
+ content.Append(child.TextContent);
+ }
+ }
+
+ return content.ToPool();
+ }
+ set => base.TextContent = value;
+ }
+
#endregion
#region Methods
diff --git a/src/AngleSharp.Xml/Dtd/Declaration/Attribute/AttributeTokenizedType.cs b/src/AngleSharp.Xml/Dtd/Declaration/Attribute/AttributeTokenizedType.cs
index 06ba99e..69132b2 100644
--- a/src/AngleSharp.Xml/Dtd/Declaration/Attribute/AttributeTokenizedType.cs
+++ b/src/AngleSharp.Xml/Dtd/Declaration/Attribute/AttributeTokenizedType.cs
@@ -44,71 +44,23 @@ public override Boolean Check(Element element)
{
case TokenizedType.ENTITIES:
{
- //TODO
- break;
+ return CheckNames(attr, false);
}
case TokenizedType.ENTITY:
{
- //TODO
- break;
+ return CheckNames(attr, true);
}
case TokenizedType.ID:
{
- if (String.IsNullOrEmpty(attr) || !attr[0].IsXmlNameStart())
- {
- return false;
- }
-
- for (int i = 1; i < attr.Length; i++)
- {
- if (!attr[i].IsXmlName())
- {
- return false;
- }
- }
-
- //TODO only one ID per element
- return true;
+ return CheckNames(attr, true);
}
case TokenizedType.IDREF:
{
- if (String.IsNullOrEmpty(attr) || !attr[0].IsXmlNameStart())
- return false;
-
- for (var i = 1; i < attr.Length; i++)
- {
- if (!attr[i].IsXmlName())
- {
- return false;
- }
- }
-
- //TODO check reference
- return true;
+ return CheckNames(attr, true);
}
case TokenizedType.IDREFS:
{
- var start = true;
-
- for (var i = 0; i < attr.Length; i++)
- {
- if (!attr[i].IsSpaceCharacter())
- {
- if (start && !attr[i].IsXmlNameStart())
- return false;
- else if (!start && !attr[i].IsXmlName())
- return false;
- else if (start)
- start = false;
- }
- else
- {
- start = true;
- }
- }
-
- //TODO check references
- return true;
+ return CheckNames(attr, false);
}
case TokenizedType.NMTOKEN:
{
@@ -139,6 +91,34 @@ public override Boolean Check(Element element)
return true;
}
+ private static Boolean CheckNames(String value, Boolean requiresSingleName)
+ {
+ var names = value.Split((Char[])null, StringSplitOptions.RemoveEmptyEntries);
+
+ if (names.Length == 0 || requiresSingleName && names.Length != 1)
+ {
+ return false;
+ }
+
+ foreach (var name in names)
+ {
+ if (String.IsNullOrEmpty(name) || !name[0].IsXmlNameStart())
+ {
+ return false;
+ }
+
+ for (var i = 1; i < name.Length; i++)
+ {
+ if (!name[i].IsXmlName())
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
#endregion
}
}
diff --git a/src/AngleSharp.Xml/Dtd/Parser/Tokens/DtdEntityToken.cs b/src/AngleSharp.Xml/Dtd/Parser/Tokens/DtdEntityToken.cs
index ce9fb83..a7932b9 100644
--- a/src/AngleSharp.Xml/Dtd/Parser/Tokens/DtdEntityToken.cs
+++ b/src/AngleSharp.Xml/Dtd/Parser/Tokens/DtdEntityToken.cs
@@ -65,7 +65,7 @@ public Entity ToElement()
{
return new Entity(null, Name)
{
- NotationName = null,
+ NotationName = ExternNotation,
NodeValue = Value
};
}
diff --git a/src/AngleSharp.Xml/Parser/XmlDomBuilder.cs b/src/AngleSharp.Xml/Parser/XmlDomBuilder.cs
index 8a6a330..729b740 100644
--- a/src/AngleSharp.Xml/Parser/XmlDomBuilder.cs
+++ b/src/AngleSharp.Xml/Parser/XmlDomBuilder.cs
@@ -30,6 +30,8 @@ sealed class XmlDomBuilder
private readonly Dictionary> _internalAttributeDeclarations;
private readonly Dictionary> _internalAttributeRules;
private readonly Dictionary _internalGeneralEntities;
+ private readonly Dictionary _fallbackUnparsedEntities;
+ private readonly HashSet _fallbackNotations;
private DtdContainer _dtd;
private String _doctypeName;
@@ -55,6 +57,8 @@ internal XmlDomBuilder(Document document)
_internalAttributeDeclarations = new Dictionary>(StringComparer.Ordinal);
_internalAttributeRules = new Dictionary>(StringComparer.Ordinal);
_internalGeneralEntities = new Dictionary(StringComparer.Ordinal);
+ _fallbackUnparsedEntities = new Dictionary(StringComparer.Ordinal);
+ _fallbackNotations = new HashSet(StringComparer.Ordinal);
_currentMode = XmlTreeMode.Initial;
}
@@ -196,6 +200,14 @@ private void Initial(XmlToken token)
var declarationToken = (XmlDeclarationToken)token;
_standalone = declarationToken.Standalone;
+ if (_document is XmlDocument xmlDocument)
+ {
+ xmlDocument.SetDeclaration(
+ declarationToken.Version,
+ declarationToken.IsEncodingMissing ? null : declarationToken.Encoding,
+ declarationToken.Standalone);
+ }
+
if (!declarationToken.IsEncodingMissing)
{
SetEncoding(declarationToken.Encoding);
@@ -331,6 +343,12 @@ private void InBody(XmlToken token)
{
var attr = otherAttributes[i];
var item = CreateAttribute(attr.Key, attr.Value.Trim());
+
+ if (item.NamespaceUri == NamespaceNames.XmlUri && item.LocalName == "id")
+ {
+ item.Value = XmlElementExtensions.NormalizeXmlId(item.Value);
+ }
+
element.AddAttribute(item);
}
@@ -387,7 +405,16 @@ private void InBody(XmlToken token)
case XmlTokenType.CData:
{
var cdataToken = (XmlCDataToken)token;
- CurrentNode.AppendText(cdataToken.Data);
+
+ if (_document is IXmlDocument xmlDocument)
+ {
+ CurrentNode.AppendChild(xmlDocument.CreateCDataSection(cdataToken.Data));
+ }
+ else
+ {
+ CurrentNode.AppendText(cdataToken.Data);
+ }
+
break;
}
case XmlTokenType.Character:
@@ -514,6 +541,8 @@ private void ParseDoctypeSubset(XmlDoctypeToken doctypeToken)
_internalAttributeDeclarations.Clear();
_internalAttributeRules.Clear();
_internalGeneralEntities.Clear();
+ _fallbackUnparsedEntities.Clear();
+ _fallbackNotations.Clear();
var hasExternalSubset = TryLoadExternalSubset(doctypeToken.SystemIdentifier, out var externalSubset, out var externalSubsetPath);
@@ -619,6 +648,10 @@ private void ParseSubsetFallbackDeclarations(String subset)
var rule = new InternalAttributeRule
{
IsRequired = defaultDeclaration.StartsWith("#REQUIRED", StringComparison.Ordinal),
+ Type = GetIdentityType(attr.Groups[2].Value),
+ IsDefaultAllowedForId = defaultDeclaration.StartsWith("#REQUIRED", StringComparison.Ordinal) ||
+ defaultDeclaration.StartsWith("#IMPLIED", StringComparison.Ordinal),
+ NotationNames = GetNotationNames(attr.Groups[2].Value),
};
if (defaultDeclaration.StartsWith("#FIXED", StringComparison.Ordinal))
@@ -643,6 +676,19 @@ private void ParseSubsetFallbackDeclarations(String subset)
var quoted = match.Groups[2].Value;
_internalGeneralEntities[entityName] = quoted.Substring(1, quoted.Length - 2);
}
+
+ foreach (Match match in Regex.Matches(
+ subset,
+ "",
+ RegexOptions.Singleline))
+ {
+ _fallbackUnparsedEntities[match.Groups[1].Value] = match.Groups[2].Value;
+ }
+
+ foreach (Match match in Regex.Matches(subset, "(_fallbackNotations, StringComparer.Ordinal);
+ var unparsedEntities = new Dictionary(_fallbackUnparsedEntities, StringComparer.Ordinal);
+
+ if (_dtd != null)
+ {
+ foreach (var notation in _dtd.Notations)
+ {
+ notationNames.Add(notation.NodeName);
+ }
+
+ foreach (var entity in _dtd.Entities)
+ {
+ if (!String.IsNullOrEmpty(entity.NotationName))
+ {
+ unparsedEntities[entity.NodeName] = entity.NotationName;
+ }
+ }
+ }
+
+ foreach (var notationName in unparsedEntities.Values)
+ {
+ if (!notationNames.Contains(notationName))
+ {
+ return false;
+ }
+ }
+
+ foreach (var elementDeclarations in declarations.Values)
+ {
+ var idDeclarations = elementDeclarations.Count(m => m.Type == IdentityAttributeType.ID);
+ var notationDeclarations = elementDeclarations.Count(m => m.Type == IdentityAttributeType.NOTATION);
+
+ if (idDeclarations > 1 || notationDeclarations > 1 ||
+ elementDeclarations.Any(m => m.Type == IdentityAttributeType.ID && !m.IsDefaultAllowedForId) ||
+ elementDeclarations.Any(m => m.Type == IdentityAttributeType.NOTATION && m.NotationNames.Any(n => !notationNames.Contains(n))))
+ {
+ return false;
+ }
+ }
+
+ var ids = new HashSet(StringComparer.Ordinal);
+ var references = new List();
+
+ if (!ValidateIdentityAttributes(root, declarations, ids, references, unparsedEntities))
+ {
+ return false;
+ }
+
+ return references.All(ids.Contains);
+ }
+
+ private Boolean ValidateIdentityAttributes(
+ Element element,
+ Dictionary> declarations,
+ HashSet ids,
+ List references,
+ Dictionary unparsedEntities)
+ {
+ if (declarations.TryGetValue(element.NodeName, out var elementDeclarations))
+ {
+ foreach (var declaration in elementDeclarations)
+ {
+ var value = element.GetAttribute(declaration.Name);
+
+ if (value == null)
+ {
+ continue;
+ }
+
+ var tokens = SplitTokens(value);
+
+ if (tokens.Count == 0 || tokens.Any(token => !IsXmlName(token)))
+ {
+ return false;
+ }
+
+ var normalizedValue = String.Join(" ", tokens);
+
+ if (!String.Equals(value, normalizedValue, StringComparison.Ordinal))
+ {
+ ((IElement)element).Attributes[declaration.Name].Value = normalizedValue;
+ }
+
+ switch (declaration.Type)
+ {
+ case IdentityAttributeType.ID:
+ if (tokens.Count != 1 || !ids.Add(tokens[0]))
+ {
+ return false;
+ }
+
+ ((XmlElement)element).IdAttribute = declaration.Name;
+ break;
+ case IdentityAttributeType.IDREF:
+ if (tokens.Count != 1)
+ {
+ return false;
+ }
+
+ references.Add(tokens[0]);
+ break;
+ case IdentityAttributeType.IDREFS:
+ references.AddRange(tokens);
+ break;
+ case IdentityAttributeType.ENTITY:
+ if (tokens.Count != 1 || !unparsedEntities.ContainsKey(tokens[0]))
+ {
+ return false;
+ }
+
+ break;
+ case IdentityAttributeType.ENTITIES:
+ if (tokens.Any(token => !unparsedEntities.ContainsKey(token)))
+ {
+ return false;
+ }
+
+ break;
+ }
+ }
+ }
+
+ foreach (var child in ((INode)element).ChildNodes)
+ {
+ if (child is Element nested && !ValidateIdentityAttributes(nested, declarations, ids, references, unparsedEntities))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private Dictionary> GetIdentityDeclarations()
+ {
+ var result = new Dictionary>(StringComparer.Ordinal);
+
+ if (_dtd != null)
+ {
+ foreach (var declaration in _dtd.Attributes)
+ {
+ foreach (var entry in declaration.Declarations)
+ {
+ var item = CreateIdentityDeclaration(entry);
+
+ if (item != null)
+ {
+ AddIdentityDeclaration(result, declaration.Name, item);
+ }
+ }
+ }
+ }
+
+ foreach (var elementRules in _internalAttributeRules)
+ {
+ foreach (var rule in elementRules.Value)
+ {
+ if (rule.Value.Type != IdentityAttributeType.None &&
+ !ContainsIdentityDeclaration(result, elementRules.Key, rule.Key))
+ {
+ AddIdentityDeclaration(result, elementRules.Key, new IdentityAttributeDeclaration
+ {
+ Name = rule.Key,
+ Type = rule.Value.Type,
+ IsDefaultAllowedForId = rule.Value.IsDefaultAllowedForId,
+ NotationNames = rule.Value.NotationNames,
+ });
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static IdentityAttributeDeclaration CreateIdentityDeclaration(AttributeDeclarationEntry entry)
+ {
+ if (entry.Type is AttributeTokenizedType tokenized)
+ {
+ return new IdentityAttributeDeclaration
+ {
+ Name = entry.Name,
+ Type = (IdentityAttributeType)Enum.Parse(typeof(IdentityAttributeType), tokenized.Value.ToString()),
+ IsDefaultAllowedForId = entry.Default is AttributeImpliedValue || entry.Default is AttributeRequiredValue,
+ };
+ }
+
+ if (entry.Type is AttributeEnumeratedType enumerated && enumerated.IsNotation)
+ {
+ return new IdentityAttributeDeclaration
+ {
+ Name = entry.Name,
+ Type = IdentityAttributeType.NOTATION,
+ NotationNames = enumerated.Names,
+ };
+ }
+
+ return null;
+ }
+
+ private static void AddIdentityDeclaration(
+ Dictionary> declarations,
+ String elementName,
+ IdentityAttributeDeclaration declaration)
+ {
+ if (!declarations.TryGetValue(elementName, out var items))
+ {
+ items = new List();
+ declarations[elementName] = items;
+ }
+
+ items.Add(declaration);
+ }
+
+ private static Boolean ContainsIdentityDeclaration(
+ Dictionary> declarations,
+ String elementName,
+ String attributeName) =>
+ declarations.TryGetValue(elementName, out var items) && items.Any(m => m.Name == attributeName);
+
+ private static List SplitTokens(String value) => value
+ .Split((Char[])null, StringSplitOptions.RemoveEmptyEntries)
+ .ToList();
+
+ private static Boolean IsXmlName(String value)
+ {
+ if (String.IsNullOrEmpty(value) || !value[0].IsXmlNameStart())
+ {
+ return false;
+ }
+
+ for (var i = 1; i < value.Length; i++)
+ {
+ if (!value[i].IsXmlName())
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static IdentityAttributeType GetIdentityType(String value)
+ {
+ var type = value.Trim();
+
+ if (type.StartsWith("NOTATION", StringComparison.Ordinal))
+ {
+ return IdentityAttributeType.NOTATION;
+ }
+
+ return Enum.TryParse(type, out IdentityAttributeType result) ? result : IdentityAttributeType.None;
+ }
+
+ private static IEnumerable GetNotationNames(String value)
+ {
+ var match = Regex.Match(value, "^NOTATION\\s*\\(([^\\)]*)\\)$");
+ return match.Success ?
+ match.Groups[1].Value.Split('|').Select(m => m.Trim()).Where(m => m.Length > 0).ToList() :
+ Enumerable.Empty();
+ }
+
private Boolean ValidateElementAgainstDtd(Element element)
{
var declaration = _dtd.Elements.FirstOrDefault(m => String.Equals(m.Name, element.NodeName, StringComparison.Ordinal));
@@ -1041,9 +1356,39 @@ private sealed class InternalAttributeRule
public String FixedValue { get; set; }
+ public IdentityAttributeType Type { get; set; }
+
+ public Boolean IsDefaultAllowedForId { get; set; }
+
+ public IEnumerable NotationNames { get; set; } = Enumerable.Empty();
+
public Boolean HasFixedValue => FixedValue != null;
}
+ private sealed class IdentityAttributeDeclaration
+ {
+ public String Name { get; set; }
+
+ public IdentityAttributeType Type { get; set; }
+
+ public Boolean IsDefaultAllowedForId { get; set; }
+
+ public IEnumerable NotationNames { get; set; } = Enumerable.Empty();
+ }
+
+ private enum IdentityAttributeType
+ {
+ None,
+ ID,
+ IDREF,
+ IDREFS,
+ ENTITY,
+ ENTITIES,
+ NMTOKEN,
+ NMTOKENS,
+ NOTATION,
+ }
+
#endregion
}
}
diff --git a/src/AngleSharp.Xml/XmlCanonicalizationExtensions.cs b/src/AngleSharp.Xml/XmlCanonicalizationExtensions.cs
new file mode 100644
index 0000000..e00d8ef
--- /dev/null
+++ b/src/AngleSharp.Xml/XmlCanonicalizationExtensions.cs
@@ -0,0 +1,40 @@
+namespace AngleSharp.Xml
+{
+ using AngleSharp.Dom;
+ using System;
+ using System.IO;
+
+ ///
+ /// Provides canonical XML serialization methods.
+ ///
+ public static class XmlCanonicalizationExtensions
+ {
+ ///
+ /// Serializes a document or rooted element subtree to canonical UTF-8 bytes.
+ ///
+ /// The document or element to canonicalize.
+ /// The canonicalization options.
+ /// The canonical UTF-8 octets without a byte-order mark.
+ public static Byte[] ToCanonicalXml(this INode node, XmlCanonicalizationOptions options = null)
+ {
+ return new XmlCanonicalizer(options).Canonicalize(node);
+ }
+
+ ///
+ /// Serializes a document or rooted element subtree to a stream as canonical UTF-8 bytes.
+ ///
+ /// The document or element to canonicalize.
+ /// The output stream, which remains open.
+ /// The canonicalization options.
+ public static void ToCanonicalXml(this INode node, Stream output, XmlCanonicalizationOptions options = null)
+ {
+ if (output == null)
+ {
+ throw new ArgumentNullException(nameof(output));
+ }
+
+ var content = node.ToCanonicalXml(options);
+ output.Write(content, 0, content.Length);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XmlCanonicalizationMode.cs b/src/AngleSharp.Xml/XmlCanonicalizationMode.cs
new file mode 100644
index 0000000..de096a7
--- /dev/null
+++ b/src/AngleSharp.Xml/XmlCanonicalizationMode.cs
@@ -0,0 +1,18 @@
+namespace AngleSharp.Xml
+{
+ ///
+ /// Defines the supported XML canonicalization algorithms.
+ ///
+ public enum XmlCanonicalizationMode
+ {
+ ///
+ /// Canonical XML Version 1.1.
+ ///
+ CanonicalXml11,
+
+ ///
+ /// Exclusive XML Canonicalization Version 1.0.
+ ///
+ ExclusiveXml10,
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XmlCanonicalizationOptions.cs b/src/AngleSharp.Xml/XmlCanonicalizationOptions.cs
new file mode 100644
index 0000000..a66f605
--- /dev/null
+++ b/src/AngleSharp.Xml/XmlCanonicalizationOptions.cs
@@ -0,0 +1,27 @@
+namespace AngleSharp.Xml
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Configures canonical XML serialization.
+ ///
+ public sealed class XmlCanonicalizationOptions
+ {
+ ///
+ /// Gets or sets the canonicalization algorithm.
+ ///
+ public XmlCanonicalizationMode Mode { get; set; } = XmlCanonicalizationMode.CanonicalXml11;
+
+ ///
+ /// Gets or sets if comments are included in the canonical output.
+ ///
+ public Boolean IncludeComments { get; set; }
+
+ ///
+ /// Gets or sets the prefixes processed inclusively by exclusive canonicalization.
+ /// Use an empty string or #default for the default namespace.
+ ///
+ public IEnumerable InclusiveNamespacePrefixes { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XmlCanonicalizer.cs b/src/AngleSharp.Xml/XmlCanonicalizer.cs
new file mode 100644
index 0000000..1d4d062
--- /dev/null
+++ b/src/AngleSharp.Xml/XmlCanonicalizer.cs
@@ -0,0 +1,546 @@
+namespace AngleSharp.Xml
+{
+ using AngleSharp.Dom;
+ using AngleSharp.Xml.Dom;
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+
+ sealed class XmlCanonicalizer
+ {
+ private const String XmlNamespace = "http://www.w3.org/XML/1998/namespace";
+ private readonly XmlCanonicalizationOptions _options;
+ private readonly HashSet _inclusivePrefixes;
+ private readonly StringBuilder _output;
+
+ public XmlCanonicalizer(XmlCanonicalizationOptions options)
+ {
+ _options = options ?? new XmlCanonicalizationOptions();
+
+ if (_options.Mode != XmlCanonicalizationMode.CanonicalXml11 &&
+ _options.Mode != XmlCanonicalizationMode.ExclusiveXml10)
+ {
+ throw new ArgumentOutOfRangeException(nameof(options), "The canonicalization mode is not supported.");
+ }
+
+ _inclusivePrefixes = new HashSet(StringComparer.Ordinal);
+ _output = new StringBuilder();
+
+ if (_options.InclusiveNamespacePrefixes != null)
+ {
+ foreach (var prefix in _options.InclusiveNamespacePrefixes)
+ {
+ _inclusivePrefixes.Add(prefix == "#default" ? String.Empty : prefix ?? String.Empty);
+ }
+ }
+ }
+
+ public Byte[] Canonicalize(INode node)
+ {
+ if (node == null)
+ {
+ throw new ArgumentNullException(nameof(node));
+ }
+
+ if (node is IDocument document)
+ {
+ WriteDocument(document);
+ }
+ else if (node is IElement element)
+ {
+ WriteElement(element, new Dictionary(StringComparer.Ordinal), true);
+ }
+ else
+ {
+ throw new ArgumentException("Only XML documents and element subtrees can be canonicalized.", nameof(node));
+ }
+
+ return new UTF8Encoding(false).GetBytes(_output.ToString());
+ }
+
+ private void WriteDocument(IDocument document)
+ {
+ var root = document.DocumentElement;
+
+ if (root == null)
+ {
+ throw new ArgumentException("The document must have a document element.", nameof(document));
+ }
+
+ var afterRoot = false;
+
+ foreach (var child in document.ChildNodes)
+ {
+ if (child == root)
+ {
+ WriteElement(root, new Dictionary(StringComparer.Ordinal), false);
+ afterRoot = true;
+ }
+ else if (child is IProcessingInstruction processing)
+ {
+ WriteOutsideDocumentElement(() => WriteProcessing(processing), afterRoot);
+ }
+ else if (_options.IncludeComments && child is IComment comment)
+ {
+ WriteOutsideDocumentElement(() => WriteComment(comment), afterRoot);
+ }
+ }
+ }
+
+ private void WriteOutsideDocumentElement(Action write, Boolean afterRoot)
+ {
+ if (afterRoot)
+ {
+ _output.Append('\n');
+ }
+
+ write();
+
+ if (!afterRoot)
+ {
+ _output.Append('\n');
+ }
+ }
+
+ private void WriteElement(IElement element, Dictionary renderedNamespaces, Boolean isSubtreeRoot)
+ {
+ var inScopeNamespaces = GetInScopeNamespaces(element);
+ ValidateNamespaces(inScopeNamespaces);
+ var namespaces = GetNamespacesToRender(element, inScopeNamespaces, renderedNamespaces);
+ var attributes = GetAttributes(element, isSubtreeRoot);
+ var qualifiedName = GetQualifiedName(element.Prefix, element.LocalName);
+
+ _output.Append('<').Append(qualifiedName);
+
+ foreach (var declaration in namespaces.OrderBy(m => m.Key, StringComparer.Ordinal))
+ {
+ _output.Append(' ').Append(String.IsNullOrEmpty(declaration.Key) ? "xmlns" : "xmlns:" + declaration.Key);
+ _output.Append("=\"");
+ WriteAttributeValue(declaration.Value);
+ _output.Append('"');
+ }
+
+ foreach (var attribute in attributes
+ .OrderBy(m => m.NamespaceUri ?? String.Empty, StringComparer.Ordinal)
+ .ThenBy(m => m.LocalName, StringComparer.Ordinal))
+ {
+ _output.Append(' ').Append(attribute.QualifiedName).Append("=\"");
+ WriteAttributeValue(attribute.Value);
+ _output.Append('"');
+ }
+
+ _output.Append('>');
+ var childNamespaces = new Dictionary(renderedNamespaces, StringComparer.Ordinal);
+
+ foreach (var declaration in namespaces)
+ {
+ childNamespaces[declaration.Key] = declaration.Value;
+ }
+
+ foreach (var child in element.ChildNodes)
+ {
+ WriteNode(child, childNamespaces);
+ }
+
+ _output.Append("").Append(qualifiedName).Append('>');
+ }
+
+ private void WriteNode(INode node, Dictionary renderedNamespaces)
+ {
+ if (node is IElement element)
+ {
+ WriteElement(element, renderedNamespaces, false);
+ }
+ else if (node is IProcessingInstruction processing)
+ {
+ WriteProcessing(processing);
+ }
+ else if (_options.IncludeComments && node is IComment comment)
+ {
+ WriteComment(comment);
+ }
+ else if (node is ICharacterData characterData && !(node is IComment))
+ {
+ WriteText(characterData.Data);
+ }
+ }
+
+ private IDictionary GetNamespacesToRender(
+ IElement element,
+ Dictionary inScope,
+ Dictionary rendered)
+ {
+ var result = new Dictionary(StringComparer.Ordinal);
+
+ if (_options.Mode == XmlCanonicalizationMode.CanonicalXml11)
+ {
+ foreach (var declaration in inScope)
+ {
+ if (declaration.Key == "xml")
+ {
+ continue;
+ }
+
+ if (!rendered.TryGetValue(declaration.Key, out var value) || value != declaration.Value)
+ {
+ if (!String.IsNullOrEmpty(declaration.Value) || rendered.ContainsKey(declaration.Key))
+ {
+ result[declaration.Key] = declaration.Value;
+ }
+ }
+ }
+ }
+ else
+ {
+ var visiblePrefixes = new HashSet(_inclusivePrefixes, StringComparer.Ordinal);
+ visiblePrefixes.Add(element.Prefix ?? String.Empty);
+
+ foreach (var attribute in element.Attributes.Where(m => !IsNamespaceDeclaration(m)))
+ {
+ if (!String.IsNullOrEmpty(attribute.Prefix) && attribute.Prefix != "xml")
+ {
+ visiblePrefixes.Add(attribute.Prefix);
+ }
+ }
+
+ foreach (var prefix in visiblePrefixes)
+ {
+ if (prefix == "xml")
+ {
+ continue;
+ }
+
+ var value = inScope.TryGetValue(prefix, out var namespaceUri) ? namespaceUri : String.Empty;
+
+ if (!rendered.TryGetValue(prefix, out var renderedValue) || renderedValue != value)
+ {
+ if (!String.IsNullOrEmpty(value) || String.IsNullOrEmpty(prefix) && rendered.ContainsKey(prefix))
+ {
+ result[prefix] = value;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private List GetAttributes(IElement element, Boolean isSubtreeRoot)
+ {
+ var attributes = element.Attributes
+ .Where(m => !IsNamespaceDeclaration(m))
+ .Select(m => new CanonicalAttribute(m.Prefix, m.LocalName, m.NamespaceUri, m.Value))
+ .ToList();
+
+ if (isSubtreeRoot && _options.Mode == XmlCanonicalizationMode.CanonicalXml11)
+ {
+ AddInheritedXmlAttribute(element, attributes, "lang");
+ AddInheritedXmlAttribute(element, attributes, "space");
+ AddFixedUpXmlBase(element, attributes);
+ }
+
+ return attributes;
+ }
+
+ private static void AddInheritedXmlAttribute(IElement element, List attributes, String localName)
+ {
+ if (attributes.Any(m => m.NamespaceUri == XmlNamespace && m.LocalName == localName))
+ {
+ return;
+ }
+
+ for (var ancestor = element.ParentElement; ancestor != null; ancestor = ancestor.ParentElement)
+ {
+ var attribute = ancestor.Attributes.FirstOrDefault(m => m.NamespaceUri == XmlNamespace && m.LocalName == localName);
+
+ if (attribute != null)
+ {
+ attributes.Add(new CanonicalAttribute("xml", localName, XmlNamespace, attribute.Value));
+ return;
+ }
+ }
+ }
+
+ private static void AddFixedUpXmlBase(IElement element, List attributes)
+ {
+ var bases = new Stack();
+ var hasAncestorBase = false;
+
+ for (var current = element; current != null; current = current.ParentElement)
+ {
+ var attribute = current.Attributes.FirstOrDefault(m => m.NamespaceUri == XmlNamespace && m.LocalName == "base");
+
+ if (attribute != null)
+ {
+ bases.Push(attribute.Value);
+ hasAncestorBase |= current != element;
+ }
+ }
+
+ if (!hasAncestorBase)
+ {
+ return;
+ }
+
+ var value = default(String);
+
+ while (bases.Count > 0)
+ {
+ value = JoinUriReferences(value, bases.Pop());
+ }
+
+ attributes.RemoveAll(m => m.NamespaceUri == XmlNamespace && m.LocalName == "base");
+
+ if (!String.IsNullOrEmpty(value))
+ {
+ attributes.Add(new CanonicalAttribute("xml", "base", XmlNamespace, value));
+ }
+ }
+
+ private static String JoinUriReferences(String baseUri, String reference)
+ {
+ reference = RemoveFragment(reference ?? String.Empty);
+
+ if (String.IsNullOrEmpty(baseUri) || HasScheme(reference))
+ {
+ return reference;
+ }
+
+ baseUri = RemoveFragment(baseUri);
+ var referenceQuery = GetQuery(reference);
+ var referencePath = RemoveQuery(reference);
+ var baseQuery = GetQuery(baseUri);
+ var basePath = RemoveQuery(baseUri);
+ var schemeEnd = basePath.IndexOf(':');
+ var scheme = schemeEnd > 0 ? basePath.Substring(0, schemeEnd + 1) : String.Empty;
+ var afterScheme = schemeEnd > 0 ? basePath.Substring(schemeEnd + 1) : basePath;
+ var authority = String.Empty;
+
+ if (referencePath.StartsWith("//", StringComparison.Ordinal))
+ {
+ return scheme + RemoveDotSegments(referencePath) + referenceQuery;
+ }
+
+ if (afterScheme.StartsWith("//", StringComparison.Ordinal))
+ {
+ var pathStart = afterScheme.IndexOf('/', 2);
+ authority = pathStart < 0 ? afterScheme : afterScheme.Substring(0, pathStart);
+ afterScheme = pathStart < 0 ? String.Empty : afterScheme.Substring(pathStart);
+ }
+
+ if (referencePath.Length == 0)
+ {
+ return scheme + authority + afterScheme + (referenceQuery.Length > 0 ? referenceQuery : baseQuery);
+ }
+
+ var path = referencePath.StartsWith("/", StringComparison.Ordinal) ?
+ referencePath :
+ MergePaths(afterScheme, referencePath, authority.Length > 0);
+ return scheme + authority + RemoveDotSegments(path) + referenceQuery;
+ }
+
+ private static String MergePaths(String basePath, String referencePath, Boolean hasAuthority)
+ {
+ if (hasAuthority && basePath.Length == 0)
+ {
+ return "/" + referencePath;
+ }
+
+ var slash = basePath.LastIndexOf('/');
+ return slash < 0 ? referencePath : basePath.Substring(0, slash + 1) + referencePath;
+ }
+
+ private static String RemoveDotSegments(String path)
+ {
+ var absolute = path.StartsWith("/", StringComparison.Ordinal);
+ var trailingSlash = path.EndsWith("/", StringComparison.Ordinal) || path.EndsWith("/.", StringComparison.Ordinal);
+ var segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ var output = new List();
+
+ foreach (var segment in segments)
+ {
+ if (segment == ".")
+ {
+ continue;
+ }
+
+ if (segment == "..")
+ {
+ if (output.Count > 0 && output[output.Count - 1] != "..")
+ {
+ output.RemoveAt(output.Count - 1);
+ }
+ else if (!absolute)
+ {
+ output.Add("..");
+ }
+
+ trailingSlash = true;
+ }
+ else
+ {
+ output.Add(segment);
+ }
+ }
+
+ var result = (absolute ? "/" : String.Empty) + String.Join("/", output);
+
+ if (trailingSlash && result.Length > 0 && !result.EndsWith("/", StringComparison.Ordinal))
+ {
+ result += "/";
+ }
+
+ return result;
+ }
+
+ private static Boolean HasScheme(String value)
+ {
+ var colon = value.IndexOf(':');
+
+ if (colon <= 0 || !Char.IsLetter(value[0]))
+ {
+ return false;
+ }
+
+ for (var i = 1; i < colon; i++)
+ {
+ if (!Char.IsLetterOrDigit(value[i]) && value[i] != '+' && value[i] != '-' && value[i] != '.')
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static String RemoveFragment(String value)
+ {
+ var hash = value.IndexOf('#');
+ return hash < 0 ? value : value.Substring(0, hash);
+ }
+
+ private static String RemoveQuery(String value)
+ {
+ var query = value.IndexOf('?');
+ return query < 0 ? value : value.Substring(0, query);
+ }
+
+ private static String GetQuery(String value)
+ {
+ var query = value.IndexOf('?');
+ return query < 0 ? String.Empty : value.Substring(query);
+ }
+
+ private static Dictionary GetInScopeNamespaces(IElement element)
+ {
+ var ancestors = new Stack();
+ var result = new Dictionary(StringComparer.Ordinal);
+
+ for (var current = element; current != null; current = current.ParentElement)
+ {
+ ancestors.Push(current);
+ }
+
+ while (ancestors.Count > 0)
+ {
+ foreach (var attribute in ancestors.Pop().Attributes.Where(IsNamespaceDeclaration))
+ {
+ var prefix = attribute.Name == "xmlns" ? String.Empty : attribute.LocalName;
+ result[prefix] = attribute.Value;
+ }
+ }
+
+ return result;
+ }
+
+ private static void ValidateNamespaces(Dictionary namespaces)
+ {
+ foreach (var namespaceUri in namespaces.Values)
+ {
+ if (!String.IsNullOrEmpty(namespaceUri) && !Uri.TryCreate(namespaceUri, UriKind.Absolute, out _))
+ {
+ throw new InvalidOperationException("Canonical XML does not permit relative namespace URIs.");
+ }
+ }
+ }
+
+ private static Boolean IsNamespaceDeclaration(IAttr attribute) =>
+ attribute.Name == "xmlns" || attribute.Prefix == "xmlns";
+
+ private void WriteAttributeValue(String value)
+ {
+ foreach (var character in value ?? String.Empty)
+ {
+ switch (character)
+ {
+ case '&': _output.Append("&"); break;
+ case '<': _output.Append("<"); break;
+ case '"': _output.Append("""); break;
+ case '\t': _output.Append(" "); break;
+ case '\n': _output.Append("
"); break;
+ case '\r': _output.Append("
"); break;
+ default: _output.Append(character); break;
+ }
+ }
+ }
+
+ private void WriteText(String value)
+ {
+ foreach (var character in value ?? String.Empty)
+ {
+ switch (character)
+ {
+ case '&': _output.Append("&"); break;
+ case '<': _output.Append("<"); break;
+ case '>': _output.Append(">"); break;
+ case '\r': _output.Append("
"); break;
+ default: _output.Append(character); break;
+ }
+ }
+ }
+
+ private void WriteProcessing(IProcessingInstruction processing)
+ {
+ _output.Append("").Append(processing.Target);
+
+ if (!String.IsNullOrEmpty(processing.Data))
+ {
+ _output.Append(' ').Append(EscapeCarriageReturns(processing.Data));
+ }
+
+ _output.Append("?>");
+ }
+
+ private void WriteComment(IComment comment)
+ {
+ _output.Append("");
+ }
+
+ private static String EscapeCarriageReturns(String value) => value?.Replace("\r", "
") ?? String.Empty;
+
+ private static String GetQualifiedName(String prefix, String localName) =>
+ String.IsNullOrEmpty(prefix) ? localName : prefix + ":" + localName;
+
+ sealed class CanonicalAttribute
+ {
+ public CanonicalAttribute(String prefix, String localName, String namespaceUri, String value)
+ {
+ Prefix = prefix;
+ LocalName = localName;
+ NamespaceUri = namespaceUri;
+ Value = value;
+ }
+
+ public String Prefix { get; }
+
+ public String LocalName { get; }
+
+ public String NamespaceUri { get; }
+
+ public String Value { get; }
+
+ public String QualifiedName => GetQualifiedName(Prefix, LocalName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XmlElementExtensions.cs b/src/AngleSharp.Xml/XmlElementExtensions.cs
new file mode 100644
index 0000000..aef5a17
--- /dev/null
+++ b/src/AngleSharp.Xml/XmlElementExtensions.cs
@@ -0,0 +1,473 @@
+namespace AngleSharp.Xml
+{
+ using AngleSharp.Dom;
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ ///
+ /// Provides convenience semantics for attributes in the XML namespace.
+ ///
+ public static class XmlElementExtensions
+ {
+ ///
+ /// Gets the effective base URL after applying inherited xml:base values.
+ ///
+ /// The element to inspect.
+ /// The effective URL, or null if no base is available.
+ public static Url GetXmlBaseUrl(this IElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ var ancestors = new Stack();
+
+ for (var current = element; current != null; current = current.ParentElement)
+ {
+ ancestors.Push(current);
+ }
+
+ var baseUrl = element.Owner?.BaseUrl;
+
+ while (ancestors.Count > 0)
+ {
+ var value = GetXmlAttribute(ancestors.Pop(), "base");
+
+ if (value != null)
+ {
+ baseUrl = baseUrl == null || baseUrl.IsInvalid ? new Url(value) : new Url(baseUrl, value);
+ }
+ }
+
+ return baseUrl;
+ }
+
+ ///
+ /// Gets the effective base URI after applying inherited xml:base values.
+ ///
+ /// The element to inspect.
+ /// The effective URI string, or an empty string if no base is available.
+ public static String GetXmlBaseUri(this IElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ var ancestors = new Stack();
+
+ for (var current = element; current != null; current = current.ParentElement)
+ {
+ ancestors.Push(current);
+ }
+
+ var baseUri = element.Owner?.BaseUri ?? String.Empty;
+
+ while (ancestors.Count > 0)
+ {
+ var value = GetXmlAttribute(ancestors.Pop(), "base");
+
+ if (value != null)
+ {
+ baseUri = ResolveUriReference(baseUri, value);
+ }
+ }
+
+ return baseUri;
+ }
+
+ ///
+ /// Gets the normalized xml:id value declared on the element.
+ ///
+ /// The element to inspect.
+ /// The normalized identifier, or null if none is declared.
+ public static String GetXmlId(this IElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ var value = GetXmlAttribute(element, "id");
+ return value == null ? null : NormalizeXmlId(value);
+ }
+
+ ///
+ /// Finds the first element in document order with the given xml:id value.
+ ///
+ /// The document to search.
+ /// The normalized identifier to find.
+ /// The matching element, or null.
+ public static IElement GetElementByXmlId(this IDocument document, String elementId)
+ {
+ if (document == null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
+ if (elementId == null)
+ {
+ throw new ArgumentNullException(nameof(elementId));
+ }
+
+ return FindByXmlId(document.DocumentElement, elementId);
+ }
+
+ ///
+ /// Gets the value of the DTD-declared ID attribute on an element.
+ ///
+ /// The element to inspect.
+ /// The declared ID value, or null.
+ public static String GetDtdId(this IElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ return element is Dom.XmlElement xmlElement && xmlElement.IdAttribute != null ?
+ element.GetAttribute(xmlElement.IdAttribute) :
+ null;
+ }
+
+ ///
+ /// Finds the first element in document order with the given DTD-declared ID.
+ ///
+ /// The document to search.
+ /// The ID to find.
+ /// The matching element, or null.
+ public static IElement GetElementByDtdId(this IDocument document, String elementId)
+ {
+ if (document == null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
+ if (elementId == null)
+ {
+ throw new ArgumentNullException(nameof(elementId));
+ }
+
+ return FindByDtdId(document.DocumentElement, elementId);
+ }
+
+ ///
+ /// Gets the effective language from the nearest xml:lang declaration.
+ ///
+ /// The element to inspect.
+ /// The effective language, an empty string when reset, or null when undeclared.
+ public static String GetXmlLanguage(this IElement element)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ for (var current = element; current != null; current = current.ParentElement)
+ {
+ var value = GetXmlAttribute(current, "lang");
+
+ if (value != null)
+ {
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ private static IElement FindByXmlId(IElement element, String elementId)
+ {
+ if (element == null)
+ {
+ return null;
+ }
+
+ if (element.GetXmlId() == elementId)
+ {
+ return element;
+ }
+
+ foreach (var child in element.Children)
+ {
+ var match = FindByXmlId(child, elementId);
+
+ if (match != null)
+ {
+ return match;
+ }
+ }
+
+ return null;
+ }
+
+ private static IElement FindByDtdId(IElement element, String elementId)
+ {
+ if (element == null)
+ {
+ return null;
+ }
+
+ if (element.GetDtdId() == elementId)
+ {
+ return element;
+ }
+
+ foreach (var child in element.Children)
+ {
+ var match = FindByDtdId(child, elementId);
+
+ if (match != null)
+ {
+ return match;
+ }
+ }
+
+ return null;
+ }
+
+ private static String GetXmlAttribute(IElement element, String localName)
+ {
+ foreach (var attribute in element.Attributes)
+ {
+ if (attribute.NamespaceUri == NamespaceNames.XmlUri && attribute.LocalName == localName)
+ {
+ return attribute.Value;
+ }
+ }
+
+ return null;
+ }
+
+ internal static String NormalizeXmlId(String value)
+ {
+ var result = new StringBuilder();
+ var pendingSpace = false;
+
+ foreach (var character in value)
+ {
+ if (character == ' ' || character == '\t' || character == '\n' || character == '\r')
+ {
+ pendingSpace = result.Length > 0;
+ }
+ else
+ {
+ if (pendingSpace)
+ {
+ result.Append(' ');
+ pendingSpace = false;
+ }
+
+ result.Append(character);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ private static String ResolveUriReference(String baseUri, String reference)
+ {
+ var baseParts = UriReference.Parse(baseUri);
+ var referenceParts = UriReference.Parse(reference);
+ var result = new UriReference
+ {
+ Fragment = referenceParts.Fragment,
+ };
+
+ if (referenceParts.Scheme != null)
+ {
+ result.Scheme = referenceParts.Scheme;
+ result.Authority = referenceParts.Authority;
+ result.Path = RemoveDotSegments(referenceParts.Path);
+ result.Query = referenceParts.Query;
+ return result.ToString();
+ }
+
+ result.Scheme = baseParts.Scheme;
+
+ if (referenceParts.Authority != null)
+ {
+ result.Authority = referenceParts.Authority;
+ result.Path = RemoveDotSegments(referenceParts.Path);
+ result.Query = referenceParts.Query;
+ }
+ else
+ {
+ result.Authority = baseParts.Authority;
+
+ if (referenceParts.Path.Length == 0)
+ {
+ result.Path = baseParts.Path;
+ result.Query = referenceParts.Query ?? baseParts.Query;
+ }
+ else
+ {
+ result.Path = referenceParts.Path[0] == '/' ?
+ RemoveDotSegments(referenceParts.Path) :
+ RemoveDotSegments(MergePaths(baseParts, referenceParts.Path));
+ result.Query = referenceParts.Query;
+ }
+ }
+
+ return result.ToString();
+ }
+
+ private static String MergePaths(UriReference baseParts, String referencePath)
+ {
+ if (baseParts.Authority != null && baseParts.Path.Length == 0)
+ {
+ return "/" + referencePath;
+ }
+
+ var slash = baseParts.Path.LastIndexOf('/');
+ return slash < 0 ? referencePath : baseParts.Path.Substring(0, slash + 1) + referencePath;
+ }
+
+ private static String RemoveDotSegments(String value)
+ {
+ var input = value;
+ var output = String.Empty;
+
+ while (input.Length > 0)
+ {
+ if (input.StartsWith("../", StringComparison.Ordinal))
+ {
+ input = input.Substring(3);
+ }
+ else if (input.StartsWith("./", StringComparison.Ordinal))
+ {
+ input = input.Substring(2);
+ }
+ else if (input.StartsWith("/./", StringComparison.Ordinal))
+ {
+ input = input.Substring(2);
+ }
+ else if (input == "/.")
+ {
+ input = "/";
+ }
+ else if (input.StartsWith("/../", StringComparison.Ordinal))
+ {
+ input = input.Substring(3);
+ output = RemoveLastSegment(output);
+ }
+ else if (input == "/..")
+ {
+ input = "/";
+ output = RemoveLastSegment(output);
+ }
+ else if (input == "." || input == "..")
+ {
+ input = String.Empty;
+ }
+ else
+ {
+ var slash = input.IndexOf('/', input[0] == '/' ? 1 : 0);
+
+ if (slash < 0)
+ {
+ output += input;
+ input = String.Empty;
+ }
+ else
+ {
+ output += input.Substring(0, slash);
+ input = input.Substring(slash);
+ }
+ }
+ }
+
+ return output;
+ }
+
+ private static String RemoveLastSegment(String value)
+ {
+ var slash = value.LastIndexOf('/');
+ return slash < 0 ? String.Empty : value.Substring(0, slash);
+ }
+
+ sealed class UriReference
+ {
+ public String Scheme { get; set; }
+
+ public String Authority { get; set; }
+
+ public String Path { get; set; }
+
+ public String Query { get; set; }
+
+ public String Fragment { get; set; }
+
+ public static UriReference Parse(String value)
+ {
+ value = value ?? String.Empty;
+ var result = new UriReference();
+ var fragment = value.IndexOf('#');
+
+ if (fragment >= 0)
+ {
+ result.Fragment = value.Substring(fragment + 1);
+ value = value.Substring(0, fragment);
+ }
+
+ var query = value.IndexOf('?');
+
+ if (query >= 0)
+ {
+ result.Query = value.Substring(query + 1);
+ value = value.Substring(0, query);
+ }
+
+ var colon = value.IndexOf(':');
+ var slash = value.IndexOf('/');
+
+ if (colon > 0 && (slash < 0 || colon < slash))
+ {
+ result.Scheme = value.Substring(0, colon);
+ value = value.Substring(colon + 1);
+ }
+
+ if (value.StartsWith("//", StringComparison.Ordinal))
+ {
+ var path = value.IndexOf('/', 2);
+ result.Authority = path < 0 ? value.Substring(2) : value.Substring(2, path - 2);
+ value = path < 0 ? String.Empty : value.Substring(path);
+ }
+
+ result.Path = value;
+ return result;
+ }
+
+ public override String ToString()
+ {
+ var result = new StringBuilder();
+
+ if (Scheme != null)
+ {
+ result.Append(Scheme).Append(':');
+ }
+
+ if (Authority != null)
+ {
+ result.Append("//").Append(Authority);
+ }
+
+ result.Append(Path);
+
+ if (Query != null)
+ {
+ result.Append('?').Append(Query);
+ }
+
+ if (Fragment != null)
+ {
+ result.Append('#').Append(Fragment);
+ }
+
+ return result.ToString();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XmlMarkupFormatter.cs b/src/AngleSharp.Xml/XmlMarkupFormatter.cs
index a914b8f..a7999b6 100644
--- a/src/AngleSharp.Xml/XmlMarkupFormatter.cs
+++ b/src/AngleSharp.Xml/XmlMarkupFormatter.cs
@@ -97,6 +97,11 @@ public virtual String Processing(IProcessingInstruction processing)
///
public virtual String Text(ICharacterData text)
{
+ if (text is Dom.IXmlCDataSection)
+ {
+ return String.Concat("");
+ }
+
var content = text.Data;
return EscapeText(content);
}
diff --git a/src/AngleSharp.Xml/XsdValidationDiagnostic.cs b/src/AngleSharp.Xml/XsdValidationDiagnostic.cs
new file mode 100644
index 0000000..ba2e4d3
--- /dev/null
+++ b/src/AngleSharp.Xml/XsdValidationDiagnostic.cs
@@ -0,0 +1,44 @@
+namespace AngleSharp.Xml
+{
+ using System;
+
+ ///
+ /// Represents an XSD schema compilation or document validation diagnostic.
+ ///
+ public sealed class XsdValidationDiagnostic
+ {
+ internal XsdValidationDiagnostic(XsdValidationSeverity severity, String message, String sourceUri, Int32 lineNumber, Int32 linePosition)
+ {
+ Severity = severity;
+ Message = message;
+ SourceUri = sourceUri;
+ LineNumber = lineNumber;
+ LinePosition = linePosition;
+ }
+
+ ///
+ /// Gets the diagnostic severity.
+ ///
+ public XsdValidationSeverity Severity { get; }
+
+ ///
+ /// Gets the diagnostic message.
+ ///
+ public String Message { get; }
+
+ ///
+ /// Gets the source URI when available.
+ ///
+ public String SourceUri { get; }
+
+ ///
+ /// Gets the one-based source line, or zero when unavailable.
+ ///
+ public Int32 LineNumber { get; }
+
+ ///
+ /// Gets the one-based source position, or zero when unavailable.
+ ///
+ public Int32 LinePosition { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XsdValidationExtensions.cs b/src/AngleSharp.Xml/XsdValidationExtensions.cs
new file mode 100644
index 0000000..ef1fa32
--- /dev/null
+++ b/src/AngleSharp.Xml/XsdValidationExtensions.cs
@@ -0,0 +1,261 @@
+namespace AngleSharp.Xml
+{
+ using AngleSharp.Dom;
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Xml;
+ using System.Xml.Schema;
+
+ ///
+ /// Provides XML Schema Definition (XSD) validation for AngleSharp documents.
+ ///
+ public static class XsdValidationExtensions
+ {
+ ///
+ /// Validates an existing XML document against one or more inline XSD schemas.
+ ///
+ /// The document to validate.
+ /// The XSD schema documents.
+ /// The validation result.
+ public static XsdValidationResult ValidateXsd(this IDocument document, params String[] schemas) =>
+ document.ValidateXsd((IEnumerable)schemas, null);
+
+ ///
+ /// Validates an existing XML document against one or more inline XSD schemas.
+ ///
+ /// The document to validate.
+ /// The XSD schema documents.
+ /// The validation options.
+ /// The validation result.
+ public static XsdValidationResult ValidateXsd(this IDocument document, IEnumerable schemas, XsdValidationOptions options)
+ {
+ if (document == null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
+ if (schemas == null)
+ {
+ throw new ArgumentNullException(nameof(schemas));
+ }
+
+ options = options ?? new XsdValidationOptions();
+ var diagnostics = new List();
+ var schemaSet = new XmlSchemaSet
+ {
+ XmlResolver = options.SchemaResolver,
+ };
+ ValidationEventHandler handler = (sender, eventArguments) =>
+ AddDiagnostic(diagnostics, eventArguments, options);
+ schemaSet.ValidationEventHandler += handler;
+ var schemaCount = 0;
+
+ try
+ {
+ foreach (var schema in schemas)
+ {
+ if (schema == null)
+ {
+ throw new ArgumentException("Schema documents cannot be null.", nameof(schemas));
+ }
+
+ schemaCount++;
+
+ using (var reader = XmlReader.Create(new StringReader(schema), CreateSchemaReaderSettings(options)))
+ {
+ schemaSet.Add(null, reader);
+ }
+ }
+
+ if (schemaCount == 0)
+ {
+ throw new ArgumentException("At least one schema document is required.", nameof(schemas));
+ }
+
+ schemaSet.Compile();
+ }
+ catch (XsdFailFastException)
+ {
+ return new XsdValidationResult(diagnostics);
+ }
+ catch (XmlSchemaException exception)
+ {
+ AddException(diagnostics, exception);
+ return new XsdValidationResult(diagnostics);
+ }
+ catch (XmlException exception)
+ {
+ AddException(diagnostics, exception);
+ return new XsdValidationResult(diagnostics);
+ }
+
+ return Validate(document, schemaSet, options, diagnostics);
+ }
+
+ ///
+ /// Validates an existing XML document against a configured schema set.
+ /// Use the schema set to assign source URIs and configure imports or includes.
+ ///
+ /// The document to validate.
+ /// The configured schema set.
+ /// The validation options.
+ /// The validation result.
+ public static XsdValidationResult ValidateXsd(this IDocument document, XmlSchemaSet schemas, XsdValidationOptions options = null)
+ {
+ if (document == null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
+ if (schemas == null)
+ {
+ throw new ArgumentNullException(nameof(schemas));
+ }
+
+ if (schemas.Count == 0)
+ {
+ throw new ArgumentException("At least one schema document is required.", nameof(schemas));
+ }
+
+ options = options ?? new XsdValidationOptions();
+ var diagnostics = new List();
+
+ if (!schemas.IsCompiled)
+ {
+ if (options.SchemaResolver != null)
+ {
+ schemas.XmlResolver = options.SchemaResolver;
+ }
+
+ ValidationEventHandler handler = (sender, eventArguments) =>
+ AddDiagnostic(diagnostics, eventArguments, options);
+ schemas.ValidationEventHandler += handler;
+
+ try
+ {
+ schemas.Compile();
+ }
+ catch (XsdFailFastException)
+ {
+ return new XsdValidationResult(diagnostics);
+ }
+ catch (XmlSchemaException exception)
+ {
+ AddException(diagnostics, exception);
+ return new XsdValidationResult(diagnostics);
+ }
+ finally
+ {
+ schemas.ValidationEventHandler -= handler;
+ }
+ }
+
+ return Validate(document, schemas, options, diagnostics);
+ }
+
+ private static XsdValidationResult Validate(IDocument document, XmlSchemaSet schemas, XsdValidationOptions options, List diagnostics)
+ {
+ var settings = new XmlReaderSettings
+ {
+ DtdProcessing = DtdProcessing.Ignore,
+ Schemas = schemas,
+ ValidationFlags = options.ValidationFlags,
+ ValidationType = ValidationType.Schema,
+ XmlResolver = null,
+ };
+
+ if (options.IsReportingWarnings)
+ {
+ settings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
+ }
+
+ settings.ValidationEventHandler += (sender, eventArguments) =>
+ AddDiagnostic(diagnostics, eventArguments, options);
+
+ try
+ {
+ var source = document.ToXml();
+ using (var reader = XmlReader.Create(new StringReader(source), settings, document.DocumentUri))
+ {
+ while (reader.Read())
+ {
+ }
+ }
+ }
+ catch (XsdFailFastException)
+ {
+ }
+ catch (XmlSchemaException exception)
+ {
+ AddException(diagnostics, exception);
+ }
+ catch (XmlException exception)
+ {
+ diagnostics.Add(new XsdValidationDiagnostic(
+ XsdValidationSeverity.Error,
+ exception.Message,
+ exception.SourceUri,
+ exception.LineNumber,
+ exception.LinePosition));
+ }
+
+ return new XsdValidationResult(diagnostics);
+ }
+
+ private static XmlReaderSettings CreateSchemaReaderSettings(XsdValidationOptions options) => new XmlReaderSettings
+ {
+ DtdProcessing = DtdProcessing.Prohibit,
+ XmlResolver = options.SchemaResolver,
+ };
+
+ private static void AddDiagnostic(List diagnostics, ValidationEventArgs eventArguments, XsdValidationOptions options)
+ {
+ var severity = eventArguments.Severity == XmlSeverityType.Warning ?
+ XsdValidationSeverity.Warning :
+ XsdValidationSeverity.Error;
+
+ if (severity == XsdValidationSeverity.Warning && !options.IsReportingWarnings)
+ {
+ return;
+ }
+
+ var exception = eventArguments.Exception;
+ diagnostics.Add(new XsdValidationDiagnostic(
+ severity,
+ eventArguments.Message,
+ exception?.SourceUri,
+ exception?.LineNumber ?? 0,
+ exception?.LinePosition ?? 0));
+
+ if (severity == XsdValidationSeverity.Error && options.IsFailFast)
+ {
+ throw new XsdFailFastException();
+ }
+ }
+
+ private static void AddException(List diagnostics, XmlSchemaException exception)
+ {
+ diagnostics.Add(new XsdValidationDiagnostic(
+ XsdValidationSeverity.Error,
+ exception.Message,
+ exception.SourceUri,
+ exception.LineNumber,
+ exception.LinePosition));
+ }
+
+ private static void AddException(List diagnostics, XmlException exception)
+ {
+ diagnostics.Add(new XsdValidationDiagnostic(
+ XsdValidationSeverity.Error,
+ exception.Message,
+ exception.SourceUri,
+ exception.LineNumber,
+ exception.LinePosition));
+ }
+
+ sealed class XsdFailFastException : Exception
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XsdValidationOptions.cs b/src/AngleSharp.Xml/XsdValidationOptions.cs
new file mode 100644
index 0000000..c9baee8
--- /dev/null
+++ b/src/AngleSharp.Xml/XsdValidationOptions.cs
@@ -0,0 +1,33 @@
+namespace AngleSharp.Xml
+{
+ using System;
+ using System.Xml;
+ using System.Xml.Schema;
+
+ ///
+ /// Configures XSD schema compilation and document validation.
+ ///
+ public sealed class XsdValidationOptions
+ {
+ ///
+ /// Gets or sets if validation stops after the first error.
+ ///
+ public Boolean IsFailFast { get; set; }
+
+ ///
+ /// Gets or sets if schema validation warnings are collected.
+ ///
+ public Boolean IsReportingWarnings { get; set; } = true;
+
+ ///
+ /// Gets or sets the resolver used for schema imports and includes.
+ /// The default is null, which disables external resource resolution.
+ ///
+ public XmlResolver SchemaResolver { get; set; }
+
+ ///
+ /// Gets or sets additional validation flags.
+ ///
+ public XmlSchemaValidationFlags ValidationFlags { get; set; } = XmlSchemaValidationFlags.ProcessIdentityConstraints;
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XsdValidationResult.cs b/src/AngleSharp.Xml/XsdValidationResult.cs
new file mode 100644
index 0000000..bbf7b2a
--- /dev/null
+++ b/src/AngleSharp.Xml/XsdValidationResult.cs
@@ -0,0 +1,27 @@
+namespace AngleSharp.Xml
+{
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// Contains the result of XSD validation.
+ ///
+ public sealed class XsdValidationResult
+ {
+ internal XsdValidationResult(IReadOnlyList diagnostics)
+ {
+ Diagnostics = diagnostics;
+ IsValid = diagnostics.All(m => m.Severity != XsdValidationSeverity.Error);
+ }
+
+ ///
+ /// Gets if validation completed without errors.
+ ///
+ public System.Boolean IsValid { get; }
+
+ ///
+ /// Gets the collected validation diagnostics.
+ ///
+ public IReadOnlyList Diagnostics { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Xml/XsdValidationSeverity.cs b/src/AngleSharp.Xml/XsdValidationSeverity.cs
new file mode 100644
index 0000000..ce4f4f8
--- /dev/null
+++ b/src/AngleSharp.Xml/XsdValidationSeverity.cs
@@ -0,0 +1,18 @@
+namespace AngleSharp.Xml
+{
+ ///
+ /// Defines the severity of an XSD validation diagnostic.
+ ///
+ public enum XsdValidationSeverity
+ {
+ ///
+ /// A non-fatal schema validation warning.
+ ///
+ Warning,
+
+ ///
+ /// A schema compilation or document validation error.
+ ///
+ Error,
+ }
+}
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 4e5ae75..3a871ae 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -2,7 +2,7 @@
Adds a powerful XML and DTD parser to AngleSharp.
AngleSharp.Xml
- 1.1.0
+ 1.2.0
latest
true
true