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 second

third

" + }; + + 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, &#160;) are kept as they + /// are: they are not collapsed, not replaced and not trimmed. HTML named entities such as &nbsp; and + /// &eacute;, 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. ///