diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ef3affd30..fc9a1a17c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -581,6 +581,7 @@ - [Added `Table.expand_column` and improved JSON deserialization.][7859] - [Implemented `Table.auto_value_types` for in-memory tables.][7908] - [Implemented Text.substring to easily select part of a Text field][7913] +- [Implemented basic XML support][7947] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -826,6 +827,7 @@ [7859]: https://github.com/enso-org/enso/pull/7859 [7908]: https://github.com/enso-org/enso/pull/7908 [7913]: https://github.com/enso-org/enso/pull/7913 +[7947]: https://github.com/enso-org/enso/pull/7947 #### Enso Compiler diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML.enso new file mode 100644 index 00000000000..8c8353c85de --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML.enso @@ -0,0 +1,399 @@ +import project.Any.Any +import project.Data.Boolean.Boolean +import project.Data.Json.Extensions +import project.Data.Json.JS_Object +import project.Data.Map.Map +import project.Data.Numbers.Integer +import project.Data.Text.Text +import project.Data.Vector.Vector +import project.Error.Error +import project.Errors.Common.Index_Out_Of_Bounds +import project.Errors.File_Error.File_Error +import project.Errors.Illegal_State.Illegal_State +import project.Errors.No_Such_Key.No_Such_Key +import project.Nothing.Nothing +import project.Panic.Panic +import project.System.File.File +import project.System.File.File_Access.File_Access +import project.System.Input_Stream.Input_Stream +from project.Data.Range.Extensions import all +from project.Data.Text.Extensions import all + +polyglot java import java.io.StringReader +polyglot java import java.lang.Exception as JException +polyglot java import javax.xml.parsers.DocumentBuilderFactory +polyglot java import javax.xml.parsers.DocumentBuilder +polyglot java import javax.xml.xpath.XPathConstants +polyglot java import javax.xml.xpath.XPathFactory +polyglot java import org.w3c.dom.Document +polyglot java import org.w3c.dom.Element +polyglot java import org.w3c.dom.Node +polyglot java import org.w3c.dom.NodeList +polyglot java import org.w3c.dom.Text as Java_Text +polyglot java import org.xml.sax.InputSource +polyglot java import org.xml.sax.SAXException +polyglot java import org.xml.sax.SAXParseException + +polyglot java import org.enso.base.XML_Utils + +type XML_Document + ## Read an XML document from a file. + + Arguments: + - file: the `File` to read the XML document from. + + If there is an error reading the file, `File_Error` is thrown. + + If there is a parsing error, `XML_Error.Parse_Error` is thrown. + + > Example + Read an XML document in 'doc.xml'. + + file = enso_project.data / "doc.xml" + doc = XML_Document.from_file test_file + from_file : File -> XML_Document ! XML_Error | File_Error + from_file file:File = + File_Error.handle_java_exceptions file <| + file.with_input_stream [File_Access.Read] XML_Document.from_stream + + ## Read an XML document from an input stream. + + Arguments: + - input_stream: the input stread to read the XML document from. + + If there is a parsing error, `XML_Error.Parse_Error` is thrown. + + > Example + Read an XML document from an input_stream. + + doc = XML_Document.from_stream input_stream + from_stream : Input_Stream -> XML_Document ! XML_Error + from_stream input_stream:Input_Stream = + XML_Error.handle_java_exceptions <| + input_stream.with_java_stream java_stream-> XML_Document.from_source java_stream + + ## Read an XML document from a string. + + Arguments: + - xml_string: The string to read the XML document from. + + If there is a parsing error, `XML_Error.Parse_Error` is thrown. + + > Example + Read an XML document from an string. + + xml_string = "" + doc = XML_Document.from_text xml_string + from_text : Text -> XML_Document ! XML_Error + from_text xml_string:Text = + XML_Error.handle_java_exceptions <| + string_reader = StringReader.new xml_string + XML_Document.from_source (InputSource.new string_reader) + + ## PRIVATE + Read XML from an input source. + from_source : Any -> XML_Document ! XML_Error + from_source input_source = + document_builder_factory = DocumentBuilderFactory.newInstance + document_builder = document_builder_factory.newDocumentBuilder + XML_Utils.setCustomErrorHandler document_builder + XML_Document.Value (document_builder.parse input_source) + + ## Get the root element of the document. + + > Example + Get the root element of a document. + + doc = XML_Document.from_file file + root = doc.root_element + root_element : XML_Element ! XML_Error + root_element self = + XML_Error.handle_java_exceptions <| + java_element = self.java_document.getDocumentElement + XML_Element.Value java_element + + ## PRIVATE + Convert to a JavaScript Object representing this XML_Document. + to_js_object : JS_Object + to_js_object self = self.root_element.to_js_object + + ## PRIVATE + Convert to a display representation of this XML_Document. + to_display_text : Text + to_display_text self = "XML_Document (" + self.root_element.to_display_text + ")" + + ## PRIVATE + Value (java_document:Document) + +type XML_Element + ## Gets the tag of an XML element. + + > Example + Get the tag of an XML element. + + XML_Document.from_text 'hello' . root_element . name + # => "foo" + name : Text ! XML_Error + name self = + XML_Error.handle_java_exceptions <| + self.java_element.getNodeName + + ## Gets a child of an XML element. + + Arguments: + - key: If an `Integer`, returns the element at position `at` in its list + of children. If a `Text`, treats `key` as an XPath specifier, and + returns the elements it points to. If a `Text` that starts with `"@"`, + returns the attribute with the given name. + + > Example + XML_Document.from_text 'hello' . root_element . get 0 + # => XML_Document.from_text "hello" . root_element + + > Example + Get a tag attribute. + + root = XML_Document.from_text 'hello' . root_element + root.get "@bar" + # => "one" + get : Text | Integer -> Any -> Any | Text | XML_Element | Vector (Text | XML_Element) ! No_Such_Key | Index_Out_Of_Bounds | XML_Error + get self key:(Text|Integer) ~if_missing=Nothing = + case key of + _ : Integer -> self.children.get key if_missing + _ : Text -> if is_attribute_key key then self.get_xpath key . get 0 if_missing else self.get_xpath key + + ## Gets a child or attribute of an XML element. + + Arguments: + - key: If an `Integer`, returns the element at position `at` in its list + of children. If a `Text`, treats `key` as an XPath specifier, and + returns the elements it points to. If a `Text` that starts with `"@"`, + returns the attribute with the given name. + + > Example + Get a nested tag: + + XML_Document.from_text 'hello' . root_element . at 0 + # => XML_Document.from_text "hello" . root_element + + > Example + Get a tag attribute. + + root = XML_Document.from_text 'hello' . root_element + root.at "@bar" + # => "one" + at : Text | Integer -> Text | XML_Element | Vector (Text | XML_Element) ! No_Such_Key | Index_Out_Of_Bounds | XML_Error + at self key:(Text|Integer) = + if_missing = case key of + _ : Integer -> Error.throw (Index_Out_Of_Bounds.Error key self.child_count) + _ : Text -> Error.throw (No_Such_Key.Error self key) + self.get key if_missing + + ## Get elements denoted by the given XPath key. + + Arguments: + - key: The XPath string to use to search for elements. + + > Example + Get an element by xpath. + + root = XML_Document.from_file test_file . root_element + root.at "/class/teacher[1]/firstname" + # => [XML_Document.from_text "Alice" . root_element] + get_xpath : Text -> Vector (Text | XML_Element) ! XML_Error + get_xpath self key:Text = + XML_Error.handle_java_exceptions <| + xpath = XPathFactory.newInstance.newXPath + only_wanted_nodes (xpath.evaluate key self.java_element XPathConstants.NODESET) + + ## Gets the child elements of an XML element. + + `children` only returns child elements and child text nodes that are not + 100% whitespace. Other node types, such as comments, are not included. + + > Example + XML_Document.from_text 'hello' . root_element . children + # => [XML_Document.from_text "hello"] + children : Vector (XML_Element | Text) ! XML_Error + children self = + XML_Error.handle_java_exceptions <| + only_wanted_nodes self.java_element.getChildNodes + + ## Gets the number children of an XML element. + + `child_count` only counts child elements and child text nodes that are + not 100% whitespace. Other node types, such as comments, are not included + in the count. + + > Example + Get the number of children of an element. + + XML_Document.from_text ' hello hello2< ' . root_element . child_count + # => 2 + child_count : Integer ! XML_Error + child_count self = self.children.length + + ## Get an attribute of an XML element. + + Arguments: + - name: The name of the attribute to get. + - if_missing: The value returned if the attribute does not exist. + + > Example + Get an attribute of an element. + + root = XML_Document.from_text 'hello' . root_element + root.attribute "bar" + # => "one" + attribute : Text -> Any -> Text | Any ! XML_Error + attribute self name:Text ~if_missing=(Error.throw (No_Such_Key.Error self name)) = + XML_Error.handle_java_exceptions <| + attr = self.java_element.getAttributeNode name + if attr.is_nothing then if_missing else attr.getValue + + ## Gets a map containing f the attributes of an XML element. + + > Example + XML_Document.from_text 'hello' . root_element . attributes + # => Map.from_vector [["bar", "one"]] + attributes : Map Text Text ! XML_Error + attributes self = + XML_Error.handle_java_exceptions <| + named_node_map = self.java_element.getAttributes + keys_and_values = 0.up_to named_node_map.getLength . map i-> + node = named_node_map.item i + [node.getNodeName, node.getNodeValue] + Map.from_vector keys_and_values + + ## Gets the text (non-markup) contents of the element and its descendants, + if any. + + > Example + Get the text content of an element. + + XML_Document.from_text 'hello' . root_element . text + # => "hello" + text : Text ! XML_Error + text self = + XML_Error.handle_java_exceptions <| + self.java_element.getTextContent + + ## Gets the raw XML of the element (including tag, attributes and contents). + + > Example + Get the outer XML of an element. + + XML_Document.from_text 'hello' . root_element . outer_xml + # => 'hello' + outer_xml : Text ! XML_Error + outer_xml self = + XML_Error.handle_java_exceptions <| + XML_Utils.outerXML self.java_element + + ## Gets the raw XML of the contents of the element, not including the + outermost tag and attributes. + + > Example + Get the inner XML of an element. + + XML_Document.from_text 'hello' . root_element . inner_xml + # => 'hello' + inner_xml : Text ! XML_Error + inner_xml self = + XML_Error.handle_java_exceptions <| + XML_Utils.innerXML self.java_element + + ## Gets elements matching a given tag name. + + This searches through all descendants of the node, not just immediate children. + + > Example + XML_Document.from_text ' hello and goodbye ' . root_element . get_elements_by_tag_name "baz" + # => [XML_Document.from_text "hello" . root_element, XML_Document.from_text "goodbye" . root_element] + get_elements_by_tag_name : Text -> Vector XML_Element ! XML_Error + get_elements_by_tag_name self tag_name:Text = + XML_Error.handle_java_exceptions <| + only_wanted_nodes (self.java_element.getElementsByTagName tag_name) + + ## PRIVATE + Convert to a display representation of this XML_Element. + to_display_text : Text + to_display_text self = 'XML_Element "' + self.name + '"' + + ## PRIVATE + Convert to a JavaScript Object representing this XML_Element. + to_js_object : JS_Object ! XML_Error + to_js_object self = + builder = Vector.new_builder 4 + builder.append ["type", "XML_Element"] + builder.append ["tag", self.name] + builder.append ["attributes", self.attributes.to_js_object] + builder.append ["children", self.children.to_js_object] + JS_Object.from_pairs builder.to_vector + + ## PRIVATE + Value (java_element:Element) + +type XML_Error + # An error that indicates that the XML data could not be parsed. + + Arguments: + - line_number: the line on which the parse failed. + - column_number: the column at which the parse failed. + Parse_Error (line_number : Integer) (column_number : Integer) + + # Any other XML-related Java exception. + Other (error : Text) + + ## PRIVATE + + Utility method for running an action with Java exceptions mapping. + handle_java_exceptions : Any -> Any ! XML_Error + handle_java_exceptions ~action = + Panic.catch JException action caught_panic-> + XML_Error.wrap_java_exception caught_panic.payload + + ## PRIVATE + + Converts a Java `Exception` into its Enso counterpart. + wrap_java_exception : JException -> XML_Error + wrap_java_exception exception:JException = case exception of + _ : SAXParseException -> Error.throw (XML_Error.Parse_Error exception.getLineNumber exception.getColumnNumber) + _ -> Error.throw (XML_Error.Other "An Exception has occurred: "+exception.to_text) + + ## PRIVATE + Convert the XML_Error to a human-readable format. + to_display_text : Text + to_display_text self = case self of + XML_Error.Parse_Error line_number column_number -> "The XML document could not be parsed at line " + line_number.to_text + ", column " + column_number.to_text + XML_Error.Other error -> error + +## PRIVATE + Filter out unwanted nodes. + Wanted nodes are: + - Elements + - Text (if not completely whitespace) + - Attribute values (which only arise in the case of XPath keys) +only_wanted_nodes : NodeList -> Vector (Text | XML_Element) +only_wanted_nodes node_list:NodeList = + nodes = 0.up_to (node_list.getLength) . map i-> + node_list.item i + is_wanted : Node -> Boolean + is_wanted node:Node = + is_element = node.getNodeType == Node.ELEMENT_NODE + is_attribute = node.getNodeType == Node.ATTRIBUTE_NODE + is_non_whitespace_text = node.getNodeType == Node.TEXT_NODE && node.getNodeValue.is_whitespace.not + is_element || is_attribute || is_non_whitespace_text + + # If an Element, wrap in XML_Element. If Java_Text, extract the string. If an attribute, extract the value. + convert node = + if node.getNodeType == Node.ELEMENT_NODE then XML_Element.Value node else + if node.getNodeType == Node.TEXT_NODE then node.getNodeValue else + if node.getNodeType == Node.ATTRIBUTE_NODE then node.getValue else + Panic.throw (Illegal_State.Error ("Unexpected child type " + node.getNodeType.to_text)) + nodes.filter is_wanted . map convert + +## PRIVATE + Returns true if `key` starts with "@". +is_attribute_key : Text -> Boolean +is_attribute_key s:Text = s.starts_with "@" diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML/XML_Format.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML/XML_Format.enso new file mode 100644 index 00000000000..cf4b607b6a3 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML/XML_Format.enso @@ -0,0 +1,50 @@ +import project.Any.Any +import project.Data.Text.Text +import project.Data.XML.XML_Document +import project.Errors.Problem_Behavior.Problem_Behavior +import project.Network.URI.URI +import project.Nothing.Nothing +import project.System.File.File +import project.System.Input_Stream.Input_Stream +from project.Data.Text.Extensions import all + +## A `File_Format` for reading and writing XML files. +type XML_Format + ## PRIVATE + If the File_Format supports reading from the file, return a configured instance. + for_file_read : File -> XML_Format | Nothing + for_file_read file:File = + case file.extension of + ".xml" -> XML_Format + _ -> Nothing + + ## PRIVATE + If this File_Format should be used for writing to that file, return a configured instance. + for_file_write : File -> XML_Format | Nothing + for_file_write file:File = + _ = [file] + Nothing + + ## PRIVATE + If the File_Format supports reading from the web response, return a configured instance. + for_web : Text -> URI|Text -> XML_Format | Nothing + for_web content_type:Text uri:(URI|Text) = + _ = [uri] + first = content_type.split ';' . first . trim + case first of + "application/xml" -> XML_Format + "text/xml" -> XML_Format + _ -> Nothing + + ## PRIVATE + Implements the `File.read` for this `File_Format` + read : File -> Problem_Behavior -> Any + read self file:File on_problems:Problem_Behavior = + _ = [on_problems] + XML_Document.from_file file + + ## PRIVATE + Implements the `Data.parse` for this `File_Format` + read_stream : Input_Stream -> Any + read_stream self stream:Input_Stream = + XML_Document.from_stream stream diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso index 8fdf9fba635..20a90f6b285 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso @@ -48,6 +48,9 @@ import project.Data.Time.Time_Of_Day.Time_Of_Day import project.Data.Time.Time_Period.Time_Period import project.Data.Time.Time_Zone.Time_Zone import project.Data.Vector.Vector +import project.Data.XML.XML_Document +import project.Data.XML.XML_Error +import project.Data.XML.XML_Format.XML_Format import project.Error.Error import project.Errors import project.Errors.Problem_Behavior.Problem_Behavior @@ -136,6 +139,9 @@ export project.Data.Time.Time_Of_Day.Time_Of_Day export project.Data.Time.Time_Period.Time_Period export project.Data.Time.Time_Zone.Time_Zone export project.Data.Vector.Vector +export project.Data.XML.XML_Document +export project.Data.XML.XML_Error +export project.Data.XML.XML_Format.XML_Format export project.Error.Error export project.Errors export project.Errors.Problem_Behavior.Problem_Behavior diff --git a/std-bits/base/src/main/java/org/enso/base/XML_Utils.java b/std-bits/base/src/main/java/org/enso/base/XML_Utils.java new file mode 100644 index 00000000000..71eb80b7919 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/XML_Utils.java @@ -0,0 +1,76 @@ +package org.enso.base; + +import java.io.ByteArrayOutputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class XML_Utils { + /** + * Return the string representation of an XML element, including its tag and all its contents. + * + * @param element the element to convert to a string + * @return the string representation of the element + * @throws TransformerException + */ + public static String outerXML(Element element) throws TransformerException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + Source source = new DOMSource(element); + Result target = new StreamResult(out); + transformer.transform(source, target); + return out.toString(); + } + + /** + * Return the string representation of the contents of an XML element, not including its tag. + * + * @param element the element to convert to a string + * @return the string representation of the element's contents + * @throws TransformerException + */ + public static String innerXML(Element element) throws TransformerException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + Result target = new StreamResult(out); + NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); ++i) { + Source source = new DOMSource(childNodes.item(i)); + transformer.transform(source, target); + } + return out.toString(); + } + + public static void setCustomErrorHandler(DocumentBuilder documentBuilder) { + documentBuilder.setErrorHandler( + new ErrorHandler() { + @Override + public void warning(SAXParseException e) throws SAXException { + ; + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + }); + } +} diff --git a/std-bits/base/src/main/java/org/enso/base/file_format/XMLFormatSPI.java b/std-bits/base/src/main/java/org/enso/base/file_format/XMLFormatSPI.java new file mode 100644 index 00000000000..a1ff3907766 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/file_format/XMLFormatSPI.java @@ -0,0 +1,14 @@ +package org.enso.base.file_format; + +@org.openide.util.lookup.ServiceProvider(service = FileFormatSPI.class) +public class XMLFormatSPI extends FileFormatSPI { + @Override + protected String getModuleName() { + return "Standard.Base.Data.XML.XML_Format"; + } + + @Override + protected String getTypeName() { + return "XML_Format"; + } +} diff --git a/test/Tests/data/xml/nested.xml b/test/Tests/data/xml/nested.xml new file mode 100644 index 00000000000..259c857a172 --- /dev/null +++ b/test/Tests/data/xml/nested.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Tests/data/xml/sample.xml b/test/Tests/data/xml/sample.xml new file mode 100644 index 00000000000..9e69328ca89 --- /dev/null +++ b/test/Tests/data/xml/sample.xml @@ -0,0 +1,35 @@ + + + + Mary + Smith + + Blah blah + + + + Bob + Jones + + This that + + + + Alice + Wright + 4.01 + + + Jessi + Cooper + 3.99 + + + Some + Randy + Extra + Brown + Text + 3.99 + + \ No newline at end of file diff --git a/test/Tests/data/xml/small.xml b/test/Tests/data/xml/small.xml new file mode 100644 index 00000000000..57f4cf6610b --- /dev/null +++ b/test/Tests/data/xml/small.xml @@ -0,0 +1,9 @@ + + + Mary + Smith + + Blah blah + + + \ No newline at end of file diff --git a/test/Tests/src/Data/XML/XML_Spec.enso b/test/Tests/src/Data/XML/XML_Spec.enso new file mode 100644 index 00000000000..d74c4bca824 --- /dev/null +++ b/test/Tests/src/Data/XML/XML_Spec.enso @@ -0,0 +1,228 @@ +from Standard.Base import all +import Standard.Base.Errors.Common.Syntax_Error +import Standard.Base.Errors.File_Error.File_Error + +from Standard.Test import Test, Test_Suite +import Standard.Test.Extensions + +spec = + test_file = enso_project.data / "xml" / "sample.xml" + document = XML_Document.from_file test_file + root = document . root_element + + fix_windows_newlines s = s.replace '\r\n' '\n' + + Test.group "Read XML" <| + Test.specify "Can read from a file" <| + root.name . should_equal "class" + + Test.specify "Error if file does not exist" <| + test_file = enso_project.data / "xml" / "sample.xmlnotexists" + XML_Document.from_file test_file . should_fail_with File_Error + + Test.specify "Can read from a stream" <| + test_file.with_input_stream [File_Access.Read] input_stream-> + doc = XML_Document.from_stream input_stream + doc.root_element.name . should_equal "class" + + Test.specify "Can read from a string" <| + xml_string = test_file.read_text + doc = XML_Document.from_text xml_string + doc.root_element.name . should_equal "class" + + Test.specify "Can read from a short string" <| + xml_string = "" + doc = XML_Document.from_text xml_string + doc.root_element.name . should_equal "class" + + Test.specify "Parse error from file" <| + test_file = enso_project.data / "sample.txt" + XML_Document.from_file test_file . catch . should_be_a XML_Error.Parse_Error + + Test.specify "Parse error from string" <| + xml_string = "<<<<Mary\n Smith\n \n Blah blah\n \n ' + fix_windows_newlines (root.at "/class/teacher[1]/bio" . at 0 . inner_xml) . should_equal '\n Blah blah\n ' + fix_windows_newlines (root.at "/class/teacher[2]/bio" . at 0 . inner_xml) . should_equal '\n This that\n ' + fix_windows_newlines (root.at "/class/teacher[2]" . at 0 . inner_xml) . should_equal '\n Bob\n Jones\n \n This that\n \n ' + + Test.specify "Can get the outer xml" <| + fix_windows_newlines (root.at "/class/teacher[1]/bio" . at 0 . outer_xml) . should_equal '\n Blah blah\n ' + fix_windows_newlines (root.at "/class/teacher[2]/bio" . at 0 . outer_xml) . should_equal '\n This that\n ' + + Test.group "get_elements_by_tag_name" <| + Test.specify "Can get elements by tag name" <| + teachers = root.get_elements_by_tag_name "teacher" + students = root.get_elements_by_tag_name "student" + gpas = root.get_elements_by_tag_name "gpa" + + teachers.length . should_equal 2 + students.length . should_equal 3 + gpas.length . should_equal 3 + + teachers.at 0 . at "@id" . should_equal "100" + teachers.at 1 . at "@id" . should_equal "101" + students.at 0 . at "@studentId" . should_equal "1000" + students.at 1 . at "@studentId" . should_equal "1001" + students.at 2 . at "@studentId" . should_equal "1002" + gpas.at 0 . text . should_equal "4.01" + gpas.at 1 . text . should_equal "3.99" + gpas.at 2 . text . should_equal "3.99" + + Test.specify "Can get nested elements" <| + test_file = enso_project.data / "xml" / "nested.xml" + root = XML_Document.from_file test_file . root_element + bars = root.get_elements_by_tag_name "bar" + bars.length . should_equal 4 + bars.map (t-> t.at "@id") . should_equal ["2", "4", "5", "6"] + + Test.specify "Can get elements by name with a wildcard" <| + root.get_elements_by_tag_name "*" . length . should_equal 20 + + Test.group "Data.read / File_Format" <| + Test.specify "Can read from a file" <| + doc = Data.read test_file + doc.root_element.name . should_equal "class" + + Test.specify "Can read from an endpoint" <| + doc = Data.fetch "https://enso-data-samples.s3.us-west-1.amazonaws.com/sample.xml" + doc.root_element.name . should_equal "class" + doc.root_element.at 1 . name . should_equal "teacher" + + Test.group "display text" <| + Test.specify "Can generate display text" <| + document.to_display_text . should_equal 'XML_Document (XML_Element "class")' + root.to_display_text . should_equal 'XML_Element "class"' + + Test.group "JSON" <| + Test.specify "Can convert to JS_Object" <| + test_file = enso_project.data / "xml" / "small.xml" + document = XML_Document.from_file test_file + root = document . root_element + expected = Json.parse ''' + { + "type": "XML_Element", + "tag": "class", + "attributes": [], + "children": [ + { + "type": "XML_Element", + "tag": "teacher", + "attributes": [ [ "id", "100" ] + ], + "children": [ + { "type": "XML_Element", "tag": "firstname", "attributes": [], "children": [ "Mary" ] }, + { "type": "XML_Element", "tag": "lastname", "attributes": [], "children": [ "Smith" ] }, + { "type": "XML_Element", "tag": "bio", "attributes": [], "children": [ "\\n Blah blah\\n " ] } + ] + } + ] + } + js = root.to_js_object + js.should_equal expected + +main = Test_Suite.run_main spec diff --git a/test/Tests/src/Main.enso b/test/Tests/src/Main.enso index 1f69d9ec46a..623d4e1535b 100644 --- a/test/Tests/src/Main.enso +++ b/test/Tests/src/Main.enso @@ -56,6 +56,7 @@ import project.Data.Text.Parse_Spec import project.Data.Text.Regex_Spec import project.Data.Text.Span_Spec import project.Data.Text.Utils_Spec +import project.Data.XML.XML_Spec import project.Data.Vector.Slicing_Helpers_Spec @@ -155,3 +156,4 @@ main = Test_Suite.run_main <| Warnings_Spec.spec System_Spec.spec Random_Spec.spec + XML_Spec.spec diff --git a/test/Tests/src/System/File_Spec.enso b/test/Tests/src/System/File_Spec.enso index 407da004931..f6e63f97daa 100644 --- a/test/Tests/src/System/File_Spec.enso +++ b/test/Tests/src/System/File_Spec.enso @@ -745,7 +745,7 @@ spec = Test.specify "should list files in a directory" <| immediate = enso_project.data.list . map .to_text - immediate.sort.should_equal (resolve ["books.json", "helloworld.txt", "sample-json.weird-extension", "sample-malformed.json", "sample.json", "sample.png", "sample.txt", "sample.xxx", "transient", "tree", "windows.log", "windows.txt"]) + immediate.sort.should_equal (resolve ["books.json", "helloworld.txt", "sample-json.weird-extension", "sample-malformed.json", "sample.json", "sample.png", "sample.txt", "sample.xxx", "transient", "tree", "windows.log", "windows.txt", 'xml']) filtered1 = enso_project.data.list name_filter="s[a-cw]mple.{t?t,md}" . map .to_text filtered1.should_equal (resolve ["sample.txt"])