From 11c39d50411a747eedac4c6a16fedf598ae798f5 Mon Sep 17 00:00:00 2001 From: HampusM Date: Thu, 23 Feb 2023 22:35:06 +0100 Subject: feat: add project & getting function entries --- .gitignore | 2 + .gitmodules | 3 + Cargo.toml | 12 +++ LICENSE-APACHE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE-MIT | 19 +++++ OpenGL-Refpages | 1 + rustfmt.toml | 11 +++ src/description.rs | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 158 ++++++++++++++++++++++++++++++++++++++ src/util.rs | 34 +++++++++ src/xml/element.rs | 186 ++++++++++++++++++++++++++++++++++++++++++++ src/xml/mod.rs | 2 + src/xml/parser.rs | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 1062 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 160000 OpenGL-Refpages create mode 100644 rustfmt.toml create mode 100644 src/description.rs create mode 100644 src/lib.rs create mode 100644 src/util.rs create mode 100644 src/xml/element.rs create mode 100644 src/xml/mod.rs create mode 100644 src/xml/parser.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f2ef3c8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "OpenGL-Refpages"] + path = OpenGL-Refpages + url = https://github.com/KhronosGroup/OpenGL-Refpages diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..538a569 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "opengl-refpages" +version = "0.1.0" +edition = "2021" + +[dependencies] +include_dir = "0.7.3" +quick-xml = "0.27.1" +thiserror = "1.0.38" + +[dev-dependencies] +pretty_assertions = "1.3.0" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..14ef2d6 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 HampusM + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..170fea6 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2023 HampusM + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/OpenGL-Refpages b/OpenGL-Refpages new file mode 160000 index 0000000..bdf33f8 --- /dev/null +++ b/OpenGL-Refpages @@ -0,0 +1 @@ +Subproject commit bdf33f8dfeab700ffcae74435b7511bb10d97f21 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..4d1e29f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,11 @@ +max_width = 90 +brace_style = "AlwaysNextLine" +group_imports = "StdExternalCrate" +wrap_comments = true +comment_width = 90 +format_code_in_doc_comments = true +imports_layout = "HorizontalVertical" +imports_granularity = "Module" +newline_style = "Unix" +reorder_impl_items = true + diff --git a/src/description.rs b/src/description.rs new file mode 100644 index 0000000..ba37abc --- /dev/null +++ b/src/description.rs @@ -0,0 +1,211 @@ +//! Reference entry description. +use crate::util::enum_with_get_inner; +use crate::xml::element::{Elements, FromElements, Tagged}; + +/// Reference entry description. +#[derive(Debug)] +pub struct Description +{ + paragraphs: Vec, +} + +impl Description +{ + /// Returns a new `ReferenceDescription`. + #[must_use] + pub fn new() -> Self + { + Self { + paragraphs: Vec::new(), + } + } + + /// Returns the reference description's paragraphs. + #[must_use] + pub fn paragraphs(&self) -> &[Paragraph] + { + &self.paragraphs + } +} + +impl Default for Description +{ + fn default() -> Self + { + Self::new() + } +} + +impl FromElements for Description +{ + type Error = Error; + + fn from_elements(elements: &Elements) -> Result + { + let paragraphs = elements + .get_all_tagged_elements_with_name("para") + .into_iter() + .map(|paragraph_element| { + Paragraph::from_elements(paragraph_element.child_elements()) + }) + .collect::, _>>()?; + + Ok(Description { paragraphs }) + } +} + +/// [`Description`] error. +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + /// Invalid paragraph. + #[error("Invalid paragraph")] + InvalidParagraph(#[from] ParagraphError), +} + +/// Reference entry description paragraph. +#[derive(Debug)] +pub struct Paragraph +{ + parts: Vec, +} + +impl Paragraph +{ + /// Returns the parts of the paragraph. + #[must_use] + pub fn parts(&self) -> &[ParagraphPart] + { + &self.parts + } +} + +impl FromElements for Paragraph +{ + type Error = ParagraphError; + + fn from_elements(elements: &Elements) -> Result + { + let parts = elements + .into_iter() + .map(|element| { + ParagraphPart::from_elements(&Elements::from([element.clone()])) + }) + .collect::, _>>()?; + + Ok(Self { parts }) + } +} + +/// [`Paragraph`] error. +#[derive(Debug, thiserror::Error)] +pub enum ParagraphError +{ + /// Invalid reference description part. + #[error("Invalid part")] + InvalidPart(#[from] ParagraphPartError), +} + +enum_with_get_inner! { +inner = String; + +/// Reference entry description paragraph part. +#[derive(Debug)] +pub enum ParagraphPart +{ + /// Text part. + Text(String), + + /// .. part. + Constant(String), + + /// .. part. + Function(String), + + /// .. part. + Parameter(String), + + /// Reference entry citation part. + Entry(String), +} +} + +impl FromElements for ParagraphPart +{ + type Error = ParagraphPartError; + + fn from_elements(elements: &Elements) -> Result + { + if let Some(tagged_element) = elements.get_first_tagged() { + return Self::from_tagged_element(tagged_element); + } + + let text = elements + .get_first_text_element() + .ok_or(Self::Error::InputIsComment)?; + + Ok(Self::Text(text.clone())) + } +} + +impl ParagraphPart +{ + fn from_tagged_element( + tagged_element: &Tagged, + ) -> Result::Error> + { + let create: fn(String) -> Self = match tagged_element.name() { + "constant" => Self::Constant, + "function" => Self::Function, + "parameter" => Self::Parameter, + "citerefentry" => Self::Entry, + _ => { + return Err(::Error::UnknownPart( + tagged_element.name().to_string(), + )); + } + }; + + if tagged_element.name() == "citerefentry" { + let title_element = tagged_element + .child_elements() + .get_first_tagged_with_name("refentrytitle") + .ok_or(::Error::NoEntryTitleFound)?; + + let title = title_element + .child_elements() + .get_first_text_element() + .ok_or(::Error::NoTextInTagged)?; + + return Ok(Self::Entry(title.clone())); + } + + let text_element = tagged_element + .child_elements() + .get_first_text_element() + .ok_or(::Error::NoTextInTagged)?; + + Ok(create(text_element.clone())) + } +} + +/// [`ParagraphPart`] error. +#[derive(Debug, thiserror::Error)] +pub enum ParagraphPartError +{ + /// Input element is a comment. + #[error("Input element is a comment")] + InputIsComment, + + /// A input element is a unknown reference description part. + #[error("Input element with name '{0}' is a unknown reference description part")] + UnknownPart(String), + + /// No text was found in tagged input element. + #[error("No text was found in tagged input element")] + NoTextInTagged, + + /// No entry title found. + #[error("No entry title found")] + NoEntryTitleFound, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8e9e1e7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,158 @@ +//! Rust API for the [OpenGL reference page sources]. +//! +//! [OpenGL reference page sources]: https://github.com/KhronosGroup/OpenGL-Refpages +#![cfg_attr(doc_cfg, feature(doc_cfg))] +#![deny(clippy::all, clippy::pedantic, missing_docs)] + +use std::os::unix::prelude::OsStrExt; + +use include_dir::{include_dir, Dir}; + +use crate::description::{Description, Error as DescriptionError}; +use crate::xml::element::{Attribute, Elements, FromElements}; +use crate::xml::parser::{Error as ParserError, Parser}; + +pub mod description; + +mod util; +mod xml; + +static GL4_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/OpenGL-Refpages/gl4"); + +/// Reference entry. +#[derive(Debug)] +pub struct ReferenceEntry +{ + purpose: String, + description: Description, +} + +impl ReferenceEntry +{ + /// Returns a function reference entry. + /// + /// # Errors + /// Returns `Err` if + /// - No reference entry file was found. + /// - Parsing the reference entry file data fails. + /// - The reference entry file data is invalid. + pub fn get_function(function_name: &str) -> Result + { + let function_file = GL4_DIR + .files() + .find_map(|file| { + if file.path().extension()?.as_bytes() != b"xml" { + return None; + } + + if function_name.starts_with(file.path().file_stem()?.to_str()?) { + Some(file) + } else { + None + } + }) + .ok_or_else(|| ReferenceEntryError::NoFileFound(function_name.to_string()))?; + + let function_ref_content = function_file.contents(); + + let mut parser = Parser::new(function_ref_content); + + let root_elements = parser.parse()?; + + ReferenceEntry::from_elements(&root_elements) + } + + /// Returns the reference entry purpose. + #[must_use] + pub fn purpose(&self) -> &str + { + &self.purpose + } + + /// Returns the reference entry description. + #[must_use] + pub fn description(&self) -> &Description + { + &self.description + } +} + +impl FromElements for ReferenceEntry +{ + type Error = ReferenceEntryError; + + fn from_elements(elements: &Elements) -> Result + { + let refentry_element = elements + .get_first_tagged_with_name("refentry") + .ok_or(ReferenceEntryError::MissingRefEntry)?; + + let refnamediv_element = refentry_element + .child_elements() + .get_first_tagged_with_name("refnamediv") + .ok_or(ReferenceEntryError::MissingRefNameDiv)?; + + let refpurpose_element = refnamediv_element + .child_elements() + .get_first_tagged_with_name("refpurpose") + .ok_or(ReferenceEntryError::MissingRefPurpose)?; + + let purpose = refpurpose_element + .child_elements() + .get_first_text_element() + .cloned() + .unwrap_or_default(); + + let description_refsect = refentry_element + .child_elements() + .get_first_tagged_with_name_and_attr( + "refsect1", + &Attribute { + key: "xml:id".to_string(), + value: b"description".to_vec(), + }, + ) + .ok_or(ReferenceEntryError::MissingDescriptionRefSect)?; + + let description = + Description::from_elements(description_refsect.child_elements())?; + + Ok(ReferenceEntry { + purpose, + description, + }) + } +} + +/// [`ReferenceEntry`] error. +#[derive(Debug, thiserror::Error)] +pub enum ReferenceEntryError +{ + /// No reference entry file was found. + #[error("No reference entry file was found for '{0}'")] + NoFileFound(String), + + /// No 'refentry' element was found. + #[error("No 'refentry' element was found")] + MissingRefEntry, + + /// No 'refnamediv' element was found. + #[error("No 'refnamediv' element was found")] + MissingRefNameDiv, + + /// No 'refpurpose' element was found. + #[error("No 'refpurpose' element was found")] + MissingRefPurpose, + + /// No 'refsect1' element was found with id 'description''. + #[error("No 'refsect1' element was found with id 'description'")] + MissingDescriptionRefSect, + + /// Invalid description. + #[error("Invalid description")] + InvalidDescription(#[from] DescriptionError), + + /// Parsing failed. + #[error("Parsing failed")] + ParsingFailed(#[from] ParserError), +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..309a471 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,34 @@ +macro_rules! enum_with_get_inner { + ( + inner = $inner: ty; + $(#[$attr: meta])* + $visibility: vis enum $enum_name: ident { + $( + $(#[$variant_attr: meta])* + $variant: ident($variant_inner: ty), + )* + } + ) => { + $(#[$attr])* + $visibility enum $enum_name { + $( + $(#[$variant_attr])* + $variant($inner) + ),* + } + + impl $enum_name { + /// Returns the inner value. + #[must_use] + pub fn inner(&self) -> &$inner { + match self { + $( + $enum_name::$variant(inner) => inner + ),* + } + } + } + }; +} + +pub(crate) use enum_with_get_inner; diff --git a/src/xml/element.rs b/src/xml/element.rs new file mode 100644 index 0000000..647fe90 --- /dev/null +++ b/src/xml/element.rs @@ -0,0 +1,186 @@ +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Elements +{ + elements: Vec, +} + +impl Elements +{ + pub fn get_first_tagged(&self) -> Option<&Tagged> + { + self.elements.iter().find_map(|element| match element { + Element::Tagged(tagged_element) => Some(tagged_element), + _ => None, + }) + } + + pub fn get_first_tagged_with_name(&self, tag_name: &str) -> Option<&Tagged> + { + self.elements.iter().find_map(|element| match element { + Element::Tagged(tagged_element) if tagged_element.name == tag_name => { + Some(tagged_element) + } + _ => None, + }) + } + + pub fn get_first_tagged_with_name_and_attr( + &self, + tag_name: &str, + attribute: &Attribute, + ) -> Option<&Tagged> + { + self.elements.iter().find_map(|element| match element { + Element::Tagged(tagged_element) + if tagged_element.name == tag_name + && tagged_element + .attributes + .iter() + .any(|attr| attr == attribute) => + { + Some(tagged_element) + } + _ => None, + }) + } + + pub fn get_all_tagged_elements_with_name(&self, tag_name: &str) -> Vec<&Tagged> + { + self.elements + .iter() + .filter_map(|element| match element { + Element::Tagged(tagged_element) if tagged_element.name == tag_name => { + Some(tagged_element) + } + _ => None, + }) + .collect() + } + + pub fn get_first_text_element(&self) -> Option<&String> + { + self.elements.iter().find_map(|element| match element { + Element::Text(text) => Some(text), + _ => None, + }) + } + + pub fn get_all_text_elements(&self) -> Vec<&String> + { + self.elements + .iter() + .filter_map(|element| match element { + Element::Text(text) => Some(text), + _ => None, + }) + .collect() + } + + pub fn has_tagged_element(&self, tag_name: &str) -> bool + { + self.elements.iter().any(|element| { + matches!( + element, + Element::Tagged(tagged_element) if tagged_element.name == tag_name + ) + }) + } +} + +impl> From for Elements +{ + fn from(into_iter: IntoIter) -> Self + { + Self { + elements: into_iter.into_iter().collect(), + } + } +} + +impl<'elements> IntoIterator for &'elements Elements +{ + type IntoIter = Iter<'elements>; + type Item = &'elements Element; + + fn into_iter(self) -> Self::IntoIter + { + Self::IntoIter { + elements: self.elements.iter(), + } + } +} + +pub struct Iter<'elements> +{ + elements: std::slice::Iter<'elements, Element>, +} + +impl<'elements> Iterator for Iter<'elements> +{ + type Item = &'elements Element; + + fn next(&mut self) -> Option + { + self.elements.next() + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Element +{ + Tagged(Tagged), + Text(String), + Comment(String), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Tagged +{ + name: String, + child_elements: Elements, + attributes: Vec, +} + +impl Tagged +{ + pub fn new( + name: &Name, + child_elements: ChildElements, + attributes: Attrs, + ) -> Self + where + Name: ToString, + ChildElements: Into, + Attrs: IntoIterator, + { + Self { + name: name.to_string(), + child_elements: child_elements.into(), + attributes: attributes.into_iter().collect(), + } + } + + pub fn name(&self) -> &str + { + &self.name + } + + pub fn child_elements(&self) -> &Elements + { + &self.child_elements + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Attribute +{ + pub key: String, + pub value: Vec, +} + +pub trait FromElements: Sized +{ + type Error; + + fn from_elements(elements: &Elements) -> Result; +} diff --git a/src/xml/mod.rs b/src/xml/mod.rs new file mode 100644 index 0000000..12368c3 --- /dev/null +++ b/src/xml/mod.rs @@ -0,0 +1,2 @@ +pub mod element; +pub mod parser; diff --git a/src/xml/parser.rs b/src/xml/parser.rs new file mode 100644 index 0000000..9cdafb1 --- /dev/null +++ b/src/xml/parser.rs @@ -0,0 +1,221 @@ +use std::io::BufRead; + +use quick_xml::events::attributes::AttrError; +use quick_xml::events::{BytesStart, BytesText, Event}; +use quick_xml::Reader; + +use crate::xml::element::{Attribute, Element, Elements, Tagged}; + +/// XML parser. +pub struct Parser +{ + reader: Reader, +} + +impl Parser +{ + pub fn new(src: Source) -> Self + { + Self { + reader: Reader::from_reader(src), + } + } + + pub fn parse(&mut self) -> Result + { + let mut buf = Vec::new(); + + let mut elements = Vec::new(); + + loop { + let event = self.reader.read_event_into(&mut buf)?; + + let element = match self.handle_event(event)? { + EventHandlingResult::Element(element) => element, + EventHandlingResult::Event(_) => { + continue; + } + EventHandlingResult::End => { + break; + } + }; + + elements.push(element); + } + + Ok(elements.into()) + } + + fn parse_text(text: &BytesText) -> Result + { + String::from_utf8(text.to_vec()).map_err(|_| Error::TextNotUTF8) + } + + fn parse_tagged(&mut self, start: &BytesStart) -> Result + { + let mut child_elements = Vec::new(); + + let mut buf = Vec::new(); + + loop { + let event = self.reader.read_event_into(&mut buf)?; + + match event { + Event::End(end) if end.name() == start.name() => { + break; + } + event => match self.handle_event(event)? { + EventHandlingResult::Element(element) => { + child_elements.push(element); + } + EventHandlingResult::End => { + return Err(Error::UnexpectedEndOfFile); + } + EventHandlingResult::Event(_) => {} + }, + } + } + + let attributes = start + .attributes() + .map(|attr_result| { + let attr = attr_result?; + + Ok(Attribute { + key: String::from_utf8(attr.key.as_ref().to_vec()) + .map_err(|_| Error::TagAttributeKeyNotUTF8)?, + value: attr.value.into_owned(), + }) + }) + .collect::, Error>>()?; + + Ok(Element::Tagged(Tagged::new( + &String::from_utf8(start.name().as_ref().to_vec()) + .map_err(|_| Error::TagNameNotUTF8)?, + child_elements, + attributes, + ))) + } + + fn handle_event<'a>( + &'a mut self, + event: Event<'a>, + ) -> Result + { + match event { + Event::Text(text) => Ok(EventHandlingResult::Element(Element::Text( + Self::parse_text(&text)?, + ))), + Event::Start(start) => { + Ok(EventHandlingResult::Element(self.parse_tagged(&start)?)) + } + Event::End(_) => Err(Error::UnexpectedTagEnd), + Event::Eof => Ok(EventHandlingResult::End), + Event::Comment(comment_text) => Ok(EventHandlingResult::Element( + Element::Comment(Self::parse_text(&comment_text)?), + )), + event => Ok(EventHandlingResult::Event(event)), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error(transparent)] + QuickXMLFailed(#[from] quick_xml::Error), + + #[error("Text is not UTF-8")] + TextNotUTF8, + + #[error("Tag name is not UTF-8")] + TagNameNotUTF8, + + #[error("Invalid attribute")] + InvalidTagAttr(#[from] AttrError), + + #[error("Tag attribute key is not UTF-8")] + TagAttributeKeyNotUTF8, + + #[error("Unexpectedly found the end of a tag")] + UnexpectedTagEnd, + + #[error("Unexpected end of file")] + UnexpectedEndOfFile, +} + +enum EventHandlingResult<'event> +{ + Element(Element), + Event(Event<'event>), + End, +} + +#[cfg(test)] +mod tests +{ + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn can_parse() + { + let mut parser = Parser::new("Hello there".as_bytes()); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(vec![Element::Tagged(Tagged::new( + &"foo", + vec![Element::Text("Hello there".to_string())], + Vec::new() + ))]) + ); + + let mut parser = Parser::new("123 Hello".as_bytes()); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(vec![Element::Tagged(Tagged::new( + &"foo", + vec![ + Element::Tagged(Tagged::new( + &"bar", + Elements::from(vec![Element::Text("123".to_string())]), + Vec::new() + )), + Element::Text(" Hello".to_string()) + ], + Vec::new() + ))]) + ); + + let mut parser = Parser::new("".as_bytes()); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(Vec::new()) + ); + + let mut parser = Parser::new( + "Hello there123".as_bytes(), + ); + + assert_eq!( + parser.parse().expect("Expected Ok"), + Elements::from(vec![Element::Tagged(Tagged::new( + &"foo", + vec![ + Element::Comment("XML is awful".to_string()), + Element::Text("Hello there".to_string()), + Element::Tagged(Tagged::new( + &"bar", + vec![Element::Text("123".to_string())], + Vec::new() + )), + ], + Vec::new() + ))]) + ); + } +} -- cgit v1.2.3-18-g5258