diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3dded6e..147ea7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp
## [Unreleased]
+### Added
+
+- `AttributeValueXHTML.ExtractNormalizedTextFromValue(bool keepLineBreaks = false)` returns the text of an XHTML value as plain text: runs of spaces, tabs and line breaks are collapsed to one space (non-breaking spaces are kept), no space is added around inline elements, and block elements such as paragraphs and list items are separated by a space or, with `keepLineBreaks`, by a line feed. The text of a `pre` element is kept as written, and HTML named entities such as ` ` are turned into their characters. `ExtractUnformattedTextFromValue` is unchanged ([#92](https://github.com/STARIONGROUP/ReqIFSharp/issues/92)).
+
### Changed
- A `REQ-IF` root element in a namespace other than the ReqIF namespace (`http://www.omg.org/spec/ReqIF/20110401/reqif.xsd`) is no longer read as a ReqIF document; it is skipped and a warning is logged. Before, such an element was read when it had no prefix. A root in no namespace is still read ([#87](https://github.com/STARIONGROUP/ReqIFSharp/issues/87)).
diff --git a/README.md b/README.md
index 0aae6d0..2755ddc 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,8 @@ Requirements Document
...
```
+For text without the extra spaces around inline elements, and optionally with a line break between paragraphs, use `ExtractNormalizedTextFromValue()` (or `ExtractNormalizedTextFromValue(keepLineBreaks: true)`).
+
## Write one just as easily
```csharp
diff --git a/ReqIFSharp.Extensions.Tests/ReqIFExtensions/SpecElementWithAttributesExtensionTestFixture.cs b/ReqIFSharp.Extensions.Tests/ReqIFExtensions/SpecElementWithAttributesExtensionTestFixture.cs
index 397dba0..9cb61fd 100644
--- a/ReqIFSharp.Extensions.Tests/ReqIFExtensions/SpecElementWithAttributesExtensionTestFixture.cs
+++ b/ReqIFSharp.Extensions.Tests/ReqIFExtensions/SpecElementWithAttributesExtensionTestFixture.cs
@@ -75,7 +75,7 @@ public void Verify_that_QueryExternalObjects_returns_expected_results()
[Test]
public async Task Verify_that_the_external_objects_of_xhtml_values_created_with_a_public_constructor_can_be_queried()
{
- // GH-98: the ILoggerFactory constructor left ExternalObjects null
+ // every public constructor creates the list of external objects, so the queries do not throw
var specObject = new SpecObject();
specObject.Values.Add(new AttributeValueXHTML { TheValue = "text" });
specObject.Values.Add(new AttributeValueXHTML(NullLoggerFactory.Instance) { TheValue = "text" });
diff --git a/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLReadTestFixture.cs b/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLReadTestFixture.cs
index e701f61..7fa7840 100644
--- a/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLReadTestFixture.cs
+++ b/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLReadTestFixture.cs
@@ -35,7 +35,7 @@ namespace ReqIFSharp.Tests
///
/// Verifies that reads THE-VALUE, THE-ORIGINAL-VALUE and DEFINITION
- /// in any order, in indented and minified documents, see GH-95.
+ /// in any order, in indented and minified documents.
///
[TestFixture]
public class AttributeValueXHTMLReadTestFixture
diff --git a/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLTestFixture.cs b/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLTestFixture.cs
index ee087da..45ed74d 100644
--- a/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLTestFixture.cs
+++ b/ReqIFSharp.Tests/AttributeValueTests/AttributeValueXHTMLTestFixture.cs
@@ -22,6 +22,7 @@ namespace ReqIFSharp.Tests
{
using System;
using System.IO;
+ using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -132,7 +133,7 @@ public void Verify_That_WriteXmlAsync_Without_Definition_Set_Throws_Serializatio
[Test]
public void Verify_that_every_public_constructor_creates_an_empty_list_of_external_objects()
{
- // GH-98: the ILoggerFactory constructor left ExternalObjects null
+ // also the ILoggerFactory constructor, which creates no list of its own
Assert.Multiple(() =>
{
Assert.That(new AttributeValueXHTML().ExternalObjects, Is.Not.Null.And.Empty);
@@ -144,7 +145,7 @@ public void Verify_that_every_public_constructor_creates_an_empty_list_of_extern
[Test]
public async Task Verify_that_a_value_created_with_the_parameterless_constructor_can_be_read()
{
- // GH-98: the parameterless constructor left the logger null, so reading threw an ArgumentNullException
+ // the parameterless constructor sets a logger, which reading uses
const string xml = "" +
"text" +
"";
@@ -266,6 +267,211 @@ public void Verify_that_raw_text_can_be_extracted_from_xtml_value()
Assert.That(unformattedText, Is.EqualTo("Description of the SpecObject that includes formatted tables and/or style: Element 1 Element 2"));
}
+ [Test]
+ public void Verify_that_normalized_text_has_no_extra_spaces_around_inline_elements()
+ {
+ // an inline element in a sentence, followed by punctuation
+ var attributeValue = new AttributeValueXHTML
+ {
+ TheValue = "brew a cup in under 30 seconds."
+ };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo("brew a cup in under 30 seconds."));
+ Assert.That(attributeValue.ExtractUnformattedTextFromValue(), Is.EqualTo("brew a cup in under 30 seconds ."), "the existing method keeps its behaviour");
+ }
+
+ [TestCase(null)]
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase(" \n\t ")]
+ public void Verify_that_the_normalized_text_of_a_value_without_text_is_empty(string value)
+ {
+ var attributeValue = new AttributeValueXHTML { TheValue = value };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.Empty);
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.Empty);
+ }
+
+ [Test]
+ public void Verify_that_the_line_breaks_and_indentation_of_the_source_are_collapsed()
+ {
+ var attributeValue = new AttributeValueXHTML
+ {
+ TheValue = "\r\n \r\n The system\tshall\r\n log every request.\r\n \r\n"
+ };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo("The system shall log every request."));
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.EqualTo("The system shall log every request."));
+ }
+
+ [TestCase("10 kg", "10\u00A0kg")]
+ [TestCase("a b", "a\u00A0\u00A0b")]
+ [TestCase(" ", "\u00A0")]
+ [TestCase("x ", "x\u00A0")]
+ [TestCase("a b", "a \u00A0 b")]
+ public void Verify_that_non_breaking_spaces_are_kept(string value, string expected)
+ {
+ var attributeValue = new AttributeValueXHTML { TheValue = value };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo(expected));
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.EqualTo(expected));
+ }
+
+ [TestCase(" plain text ", "plain text", Description = "a value without elements")]
+ [TestCase("ab", "ab", Description = "processing instructions are not text")]
+ [TestCase("see link", "see link", Description = "attribute values are not text")]
+ [TestCase("see x", "see x", Description = "an HTML entity in an attribute value does not throw")]
+ [TestCase("x y", "x y", Description = "xml:space=\"preserve\" gives significant whitespace, which is kept as one space")]
+ [TestCase("bold italic\nspan", "bold italic span")]
+ [TestCase("a b", "a b")]
+ public void Verify_that_whitespace_between_inline_elements_is_kept(string value, string expected)
+ {
+ // the whitespace between the elements is a text node of its own
+ var attributeValue = new AttributeValueXHTML { TheValue = value };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo(expected));
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.EqualTo(expected));
+ }
+
+ [TestCase("a b", "a b", "a b")]
+ [TestCase("line 1\nline 2", "line 1 line 2", "line 1\nline 2")]
+ [TestCase("line 1\r\nline 2", "line 1 line 2", "line 1\nline 2")]
+ [TestCase("line 1\rline 2", "line 1 line 2", "line 1\nline 2", Description = "a carriage return alone is a line break too")]
+ [TestCase("abc d", "a b c d", "a\nb\nc d", Description = "the text after a nested pre is still kept as written")]
+ [TestCase("\n code\n\tmore\n", " code \tmore", " code\n\tmore")]
+ [TestCase("a\n\nb", "a b", "a\n\nb")]
+ [TestCase("a\n \nb", "a b", "a\n \nb", Description = "a line of spaces inside the block is kept as written")]
+ [TestCase(" z", " z", " \nz", Description = "a non-breaking space is not a blank line")]
+ [TestCase("ab", "a b", "a\nb")]
+ [TestCase("x y z", "x y z", "x y z")]
+ [TestCase("before it kept after it", "before it kept after it", "before it\n kept \nafter it")]
+ [TestCase("ab", "a b", "a\nb")]
+ [TestCase(" \n \t ", "", "")]
+ [TestCase("", "", "")]
+ [TestCase("a b", "a b", "a b")]
+ public void Verify_that_the_text_of_a_pre_element_is_kept_as_written(string value, string expected, string expectedWithLineBreaks)
+ {
+ var attributeValue = new AttributeValueXHTML { TheValue = value };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo(expected));
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.EqualTo(expectedWithLineBreaks));
+ }
+
+ [TestCase("firstsecond", "first second", "first\nsecond")]
+ [TestCase("introonetwoend", "intro one two end", "intro\none\ntwo\nend")]
+ [TestCase("line oneline two", "line one line two", "line one\nline two")]
+ [TestCase("Titletext", "Title text", "Title\ntext")]
+ [TestCase("abcd", "a b c d", "a b\nc d")]
+ [TestCase("nested", "nested", "nested")]
+ [TestCase("abcde", "a b c d e", "a\nb\nc\nd\ne")]
+ [TestCase("x
y
", "x y", "x\ny")]
+ [TestCase("ABCDEF", "A B C D E F", "A\nB\nC\nD\nE\nF")]
+ [TestCase("caphbf", "cap h b f", "cap\nh\nb\nf")]
+ public void Verify_that_block_elements_separate_lines(string value, string expected, string expectedWithLineBreaks)
+ {
+ var attributeValue = new AttributeValueXHTML { TheValue = value };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo(expected));
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.EqualTo(expectedWithLineBreaks));
+ }
+
+ [Test]
+ public void Verify_that_normalized_text_reads_every_top_level_element_cdata_and_no_comments()
+ {
+ var attributeValue = new AttributeValueXHTML
+ {
+ TheValue = "first secondthird
"
+ };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.EqualTo("first a < b\nsecond\nthird"));
+ }
+
+ [Test]
+ public void Verify_that_the_normalized_text_of_a_value_that_is_not_well_formed_throws()
+ {
+ var attributeValue = new AttributeValueXHTML { TheValue = "unclosed" };
+
+ Assert.That(() => attributeValue.ExtractNormalizedTextFromValue(), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Verify_that_character_and_entity_references_are_expanded()
+ {
+ var attributeValue = new AttributeValueXHTML
+ {
+ TheValue = "a & b < c > d "e" 'f' — — g"
+ };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo("a & b < c > d \"e\" 'f' — — g"));
+ }
+
+ [TestCase("a b", "a b")]
+ [TestCase("café … ©", "café … ©")]
+ [TestCase(" x ", " x ")]
+ [TestCase("a é", "a é")]
+ public void Verify_that_html_named_entities_are_decoded(string value, string expected)
+ {
+ // XHTML in ReqIF has no DTD that declares the HTML entities, so the reader does not expand them
+ var attributeValue = new AttributeValueXHTML { TheValue = value };
+
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(), Is.EqualTo(expected));
+ Assert.That(attributeValue.ExtractNormalizedTextFromValue(true), Is.EqualTo(expected));
+ }
+
+ // the position is that of the entity name in the value, 1-based, on the first line and on later lines
+ [TestCase("a &unknown; b", 1, 13)]
+ [TestCase("x\n\na &unknown; b", 3, 13)]
+ public void Verify_that_an_unknown_entity_throws_with_its_position(string value, int lineNumber, int linePosition)
+ {
+ var attributeValue = new AttributeValueXHTML { TheValue = value };
+
+ var exception = Assert.Throws(() => attributeValue.ExtractNormalizedTextFromValue());
+
+ Assert.That(exception.Message, Does.Contain("&unknown;"));
+ Assert.That(exception.LineNumber, Is.EqualTo(lineNumber));
+ Assert.That(exception.LinePosition, Is.EqualTo(linePosition));
+ }
+
+ [Test]
+ public void Verify_that_a_value_that_is_not_well_formed_throws_with_its_position_in_the_value()
+ {
+ // the name of the closing tag, on column 13, does not match the open element
+ var attributeValue = new AttributeValueXHTML { TheValue = "a" };
+
+ var exception = Assert.Throws(() => attributeValue.ExtractNormalizedTextFromValue());
+
+ Assert.That(exception.LineNumber, Is.EqualTo(1));
+ Assert.That(exception.LinePosition, Is.EqualTo(13));
+ }
+
+ [Test]
+ public void Verify_that_a_document_type_declaration_in_the_value_is_rejected()
+ {
+ // the value is read as an XML fragment, in which a DOCTYPE is not allowed, so it is rejected whatever the
+ // DTD setting; DtdProcessing.Prohibit stays as a second line of defence
+ var attributeValue = new AttributeValueXHTML { TheValue = "]>&e;
" };
+
+ Assert.That(() => attributeValue.ExtractNormalizedTextFromValue(), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Verify_that_the_normalized_text_of_the_xhtml_values_of_a_test_document_is_normalized()
+ {
+ var reqif = new ReqIFDeserializer().Deserialize(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", "reqifsharpgenerated.reqif")).Single();
+
+ var values = reqif.CoreContent.SpecObjects.SelectMany(x => x.Values).OfType().ToList();
+
+ Assert.That(values, Is.Not.Empty);
+
+ foreach (var text in values.Select(x => x.ExtractNormalizedTextFromValue()))
+ {
+ Assert.That(text, Is.Not.Empty);
+ Assert.That(text, Does.Not.Contain(" ").And.Not.Contain("\n").And.Not.StartWith(" ").And.Not.EndWith(" "));
+ Assert.That(text, Does.StartWith("XhtmlPType").And.Contain("text before br text after br"));
+ Assert.That(text, Does.Not.Contain("eclipse.org").And.Not.Contain("text-decoration"), "attribute values are not text");
+ }
+ }
+
[Test]
public void Verify_that_when_IsSimpe_is_incorrect_bool_exception_is_thrown()
{
diff --git a/ReqIFSharp.Tests/ForeignNamespaceTestFixture.cs b/ReqIFSharp.Tests/ForeignNamespaceTestFixture.cs
index 5d86cf5..a7c33bf 100644
--- a/ReqIFSharp.Tests/ForeignNamespaceTestFixture.cs
+++ b/ReqIFSharp.Tests/ForeignNamespaceTestFixture.cs
@@ -39,7 +39,7 @@ namespace ReqIFSharp.Tests
///
/// Verifies that an element with the local name of a ReqIF element, but in another namespace, is not read as
- /// a ReqIF element: it is skipped, with its content, and a warning is logged, see GH-97.
+ /// a ReqIF element: it is skipped, with its content, and a warning is logged.
///
[TestFixture]
public class ForeignNamespaceTestFixture
@@ -334,7 +334,7 @@ public async Task Verify_that_an_attribute_value_created_with_a_logger_factory_l
[TestCase(true)]
public async Task Verify_that_an_element_in_no_namespace_in_a_document_in_the_ReqIF_namespace_is_read(bool async)
{
- // elements in no namespace are read as ReqIF elements in any document, as they were before GH-97
+ // elements in no namespace are read as ReqIF elements in any document
var document = XDocument.Parse(TestDocuments.Read(FileName));
var title = document.Root!.Descendants(TestDocuments.ReqIFNamespace + "TITLE").Single();
title.AddAfterSelf(new XElement(XNamespace.None + "TITLE", "title in no namespace"));
diff --git a/ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs b/ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs
index 9a0a90d..0b1f44d 100644
--- a/ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs
+++ b/ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs
@@ -39,7 +39,7 @@ namespace ReqIFSharp.Tests
///
/// Verifies that a document without whitespace between its elements (minified XML) is read
- /// the same as the indented original, see GH-94.
+ /// the same as the indented original.
///
[TestFixture]
public class MinifiedDocumentTestFixture
diff --git a/ReqIFSharp.Tests/NamespaceRetentionTestFixture.cs b/ReqIFSharp.Tests/NamespaceRetentionTestFixture.cs
index 9dfcf45..acc1e13 100644
--- a/ReqIFSharp.Tests/NamespaceRetentionTestFixture.cs
+++ b/ReqIFSharp.Tests/NamespaceRetentionTestFixture.cs
@@ -35,7 +35,7 @@ namespace ReqIFSharp.Tests
///
/// Suite of tests that verify the namespace declarations and root attributes of a
- /// document are retained when it is deserialized and serialized to a new destination (issue #44).
+ /// document are retained when it is deserialized and serialized to a new destination.
///
[TestFixture]
public class NamespaceRetentionTestFixture
diff --git a/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs b/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs
index 448bf50..ad39938 100644
--- a/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs
+++ b/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs
@@ -36,7 +36,7 @@ namespace ReqIFSharp.Tests
///
/// Verifies that the REQ-IF root and the elements below it are matched on their local name and namespace,
/// so that a document in which the ReqIF namespace is bound to a prefix (reqif:REQ-IF) is read
- /// the same as the unprefixed original, see GH-87.
+ /// the same as the unprefixed original.
///
[TestFixture]
public class PrefixedNamespaceTestFixture
diff --git a/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs b/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs
index 7d9c3bf..a55822a 100644
--- a/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs
+++ b/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs
@@ -25,6 +25,7 @@ namespace ReqIFSharp
using System.Diagnostics;
using System.IO;
using System.Linq;
+ using System.Net;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
@@ -45,6 +46,23 @@ public class AttributeValueXHTML : AttributeValue
///
private readonly ILogger logger;
+ ///
+ /// The XHTML elements that start and end a line in
+ ///
+ private static readonly HashSet LineElements = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "address", "blockquote", "br", "caption", "dd", "div", "dl", "dt", "h1", "h2", "h3", "h4", "h5", "h6",
+ "hr", "li", "ol", "p", "pre", "table", "tbody", "tfoot", "thead", "tr", "ul"
+ };
+
+ ///
+ /// The XHTML elements that are separated by a space in
+ ///
+ private static readonly HashSet CellElements = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "td", "th"
+ };
+
///
/// Initializes a new instance of the class.
///
@@ -222,6 +240,208 @@ public string ExtractUnformattedTextFromValue()
return result.Any() ? string.Join(" ", result) : string.Empty;
}
+ ///
+ /// Extracts the text of the value as plain text with normalized whitespace: outside pre elements, every run
+ /// of whitespace (space, tab, carriage return and line feed) is collapsed to one space, and each line is trimmed.
+ /// Text is joined as it is written in the XHTML, so no space is added around inline elements such as b or
+ /// span.
+ ///
+ ///
+ /// Block elements (such as p, div, li, the headings and table rows) and br separate
+ /// lines, and table cells are separated by a space. Unlike , all
+ /// top-level elements of the value are read. Non-breaking spaces (U+00A0,  ) are kept as they
+ /// are: they are not collapsed, not replaced and not trimmed. HTML named entities such as and
+ /// é, which no DTD declares in ReqIF, are turned into their characters.
+ ///
+ /// The text of a pre element is kept as written: its spaces, tabs and indentation are not collapsed, and
+ /// each of its lines is a line of the result. Blank lines at the start and end of a pre element are left
+ /// out, and blank lines inside it are kept only when is true.
+ ///
+ ///
+ ///
+ /// true to separate the lines with a line feed (\n); false to separate them with a space
+ ///
+ ///
+ /// The plain text; an empty string when the value is empty
+ ///
+ ///
+ /// The value is not well-formed XML, or it uses an entity that is neither an XML entity nor an HTML named entity
+ ///
+ public string ExtractNormalizedTextFromValue(bool keepLineBreaks = false)
+ {
+ if (string.IsNullOrEmpty(this.TheValue))
+ {
+ return string.Empty;
+ }
+
+ // every line, with whether it is part of a pre block, whose whitespace is kept as written
+ var lines = new List<(StringBuilder Text, bool Preformatted)> { (new StringBuilder(), false) };
+ var preDepth = 0;
+
+ // the value is read as a fragment: it may hold several top-level elements and text, and prefixes without a
+ // namespace declaration; line and position of an error are those in the value
+ using (var xmlTextReader = new XmlTextReader(this.TheValue, XmlNodeType.Element, new XmlParserContext(null, null, null, XmlSpace.None)))
+ {
+ xmlTextReader.Namespaces = false;
+ xmlTextReader.DtdProcessing = DtdProcessing.Prohibit;
+ xmlTextReader.XmlResolver = null;
+ xmlTextReader.WhitespaceHandling = WhitespaceHandling.All;
+
+ while (xmlTextReader.Read())
+ {
+ switch (xmlTextReader.NodeType)
+ {
+ case XmlNodeType.Text:
+ case XmlNodeType.CDATA:
+ case XmlNodeType.Whitespace:
+ case XmlNodeType.SignificantWhitespace:
+ AppendText(lines, xmlTextReader.Value, preDepth > 0);
+ break;
+ case XmlNodeType.EntityReference:
+ // an HTML named entity such as , which no DTD declares: the reader does not expand it
+ AppendText(lines, DecodeHtmlEntity(xmlTextReader), preDepth > 0);
+ break;
+ case XmlNodeType.Element:
+ case XmlNodeType.EndElement:
+ var name = xmlTextReader.Name.Substring(xmlTextReader.Name.LastIndexOf(':') + 1);
+
+ if (string.Equals(name, "pre", StringComparison.OrdinalIgnoreCase) && !xmlTextReader.IsEmptyElement)
+ {
+ preDepth += xmlTextReader.NodeType == XmlNodeType.Element ? 1 : -1;
+ }
+
+ if (LineElements.Contains(name))
+ {
+ lines.Add((new StringBuilder(), preDepth > 0));
+ }
+ else if (CellElements.Contains(name))
+ {
+ lines[lines.Count - 1].Text.Append(' ');
+ }
+
+ break;
+ }
+ }
+ }
+
+ var result = new List();
+ var index = 0;
+
+ while (index < lines.Count)
+ {
+ if (!lines[index].Preformatted)
+ {
+ var line = CollapseWhitespace(lines[index].Text.ToString());
+
+ if (line.Length > 0)
+ {
+ result.Add(line);
+ }
+
+ index++;
+ continue;
+ }
+
+ // a pre block: its lines as written, without the blank lines at its start and end
+ var block = new List();
+
+ while (index < lines.Count && lines[index].Preformatted)
+ {
+ block.Add(lines[index].Text.ToString());
+ index++;
+ }
+
+ var first = block.FindIndex(x => !IsBlank(x));
+
+ if (first < 0)
+ {
+ continue;
+ }
+
+ var kept = block.GetRange(first, block.FindLastIndex(x => !IsBlank(x)) - first + 1);
+
+ // on one line, the blank lines inside the block would only add spaces
+ result.AddRange(keepLineBreaks ? kept : kept.Where(x => !IsBlank(x)));
+ }
+
+ return string.Join(keepLineBreaks ? "\n" : " ", result);
+ }
+
+ ///
+ /// Appends text to the last line; in a pre block, every line break of the text starts a new line
+ ///
+ private static void AppendText(List<(StringBuilder Text, bool Preformatted)> lines, string text, bool preformatted)
+ {
+ if (!preformatted)
+ {
+ lines[lines.Count - 1].Text.Append(text);
+ return;
+ }
+
+ var parts = text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
+ lines[lines.Count - 1].Text.Append(parts[0]);
+
+ for (var i = 1; i < parts.Length; i++)
+ {
+ lines.Add((new StringBuilder(parts[i]), true));
+ }
+ }
+
+ ///
+ /// Returns the character(s) of the HTML named entity that the reader is on, such as nbsp or eacute
+ ///
+ ///
+ /// The entity is not an HTML named entity
+ ///
+ private static string DecodeHtmlEntity(XmlTextReader reader)
+ {
+ var reference = $"&{reader.Name};";
+ var decoded = WebUtility.HtmlDecode(reference);
+
+ if (decoded == reference)
+ {
+ throw new XmlException($"The entity {reference} is not an XML or HTML entity", null, reader.LineNumber, reader.LinePosition);
+ }
+
+ return decoded;
+ }
+
+ ///
+ /// Checks whether a line holds only XML whitespace (space, tab, carriage return, line feed), or nothing
+ ///
+ private static bool IsBlank(string line)
+ {
+ return CollapseWhitespace(line).Length == 0;
+ }
+
+ ///
+ /// Collapses every run of XML whitespace (space, tab, carriage return, line feed) to one space and trims the result
+ ///
+ private static string CollapseWhitespace(string text)
+ {
+ var result = new StringBuilder(text.Length);
+ var whitespace = false;
+
+ foreach (var character in text)
+ {
+ if (character == ' ' || character == '\t' || character == '\r' || character == '\n')
+ {
+ whitespace = true;
+ continue;
+ }
+
+ if (whitespace && result.Length > 0)
+ {
+ result.Append(' ');
+ }
+
+ whitespace = false;
+ result.Append(character);
+ }
+
+ return result.ToString();
+ }
+
///
/// Generates a object from its XML representation.
///