From 104ee379333c8ac615828a00aff459a658ca65a2 Mon Sep 17 00:00:00 2001 From: Jacob Lifshay Date: Fri, 2 Jan 2026 02:47:21 -0800 Subject: [PATCH] port more to rust --- src/main.rs | 578 ++++++++++++++++++++++++++++++++++++++++++++++++ src/xml_tree.rs | 198 +++++++++++++++++ 2 files changed, 776 insertions(+) create mode 100644 src/xml_tree.rs diff --git a/src/main.rs b/src/main.rs index 73c6bb9..059344a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,16 @@ // SPDX-License-Identifier: LGPL-3.0-or-later // See Notices.txt for copyright information + use non_nan_float::NonNaNF32; +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet}, + fmt, + sync::OnceLock, +}; mod quad_tree; +mod xml_tree; mod non_nan_float { #[derive(Default, PartialEq, PartialOrd, Clone, Copy)] @@ -44,6 +52,14 @@ mod non_nan_float { self.partial_cmp(other).expect("known to be non-NaN") } } + + impl std::ops::Neg for NonNaNF32 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(-self.0) + } + } } macro_rules! make_enum_font { @@ -514,4 +530,566 @@ impl Font { } } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Char { + font: Font, + text: String, + adv: NonNaNF32, + min_x: NonNaNF32, + min_y: NonNaNF32, + max_x: NonNaNF32, + max_y: NonNaNF32, +} + +impl Char { + fn width(&self) -> f32 { + self.max_x.get() - self.min_x.get() + } + fn height(&self) -> f32 { + self.max_y.get() - self.min_y.get() + } + fn top_down_left_to_right_sort_key(&self) -> impl Ord { + (-self.min_y, self.min_x) + } +} + +const COLUMN_SPLIT_X: f32 = 300.0; +const PAGE_BODY_MAX_X: f32 = 600.0; +const PAGE_BODY_MIN_X: f32 = 50.0; +const PAGE_BODY_MAX_Y: f32 = 780.0; +const PAGE_BODY_MIN_Y: f32 = 45.0; +const ONE_TITLE_LINE_SPLIT_Y: f32 = 734.0; +const TWO_TITLE_LINES_SPLIT_Y: f32 = 715.0; +const INSN_BIT_FIELDS_PREFIX_TEXT_TOP_PAD_HEIGHT: f32 = 29.938; +const INSN_BIT_FIELDS_AFFIX_TEXT_TO_BOX_TOP_HEIGHT: f32 = 9.278; +const INSN_BIT_FIELDS_PREFIX_BOX_BOTTOM_TO_SUFFIX_TEXT_HEIGHT: f32 = 20.971; +const INSN_BIT_FIELDS_TOP_PAD_HEIGHT: f32 = 20.175; +const INSN_BIT_FIELDS_TOP_PAD_HEIGHT2: f32 = 14.694; +const INSN_BIT_FIELDS_BOX_HEIGHT: f32 = 22.317; +const INSN_SP_REGS_ALTERED_REGISTER_COLUMN_X: f32 = 34.405; +const INSN_SP_REGS_ALTERED_FIELDS_COLUMN_X: f32 = 86.692; +const INSN_SP_REGS_ALTERED_FIELDS_CONDS_SPLIT_X: f32 = 188.74; + +#[derive(Clone)] +struct ParsedTextLine { + element: xml_tree::Element, + regular_min_y: f32, + regular_max_y: f32, + fonts: TextLineFonts, + chars: Vec, + preceding_blank_lines: u32, +} + +impl ParsedTextLine { + fn regular_height(&self) -> f32 { + self.regular_max_y - self.regular_min_y + } + fn get_header_text(&self) -> Option { + assert_eq!(self.fonts, TextLineFonts::InsnDescFonts); + if !self.element.text.trim().is_empty() { + return None; + } + if !self.element.tail.trim().is_empty() { + return None; + } + let [b] = &*self.element.children else { + return None; + }; + if b.tag.normal() != Some("b") { + return None; + } + if b.children.len() != 0 { + return None; + } + let text = self.element.inner_text(); + // should also check titlecase, but rust doesn't include that in std + if text.ends_with(":") && text.chars().next().is_some_and(|ch| ch.is_uppercase()) { + Some(text) + } else { + None + } + } + fn write_xml(&self, parent: &mut xml_tree::Element, trailing_nl: bool) { + for _ in 0..self.preceding_blank_lines { + parent.sub_element("br".into(), []).tail = "\n".into(); + } + if let Some(last_child) = parent.children.last_mut() { + last_child.tail += &self.element.text; + } else { + parent.text += &self.element.text; + } + parent.children.extend_from_slice(&self.element.children); + if trailing_nl { + parent.sub_element("br".into(), []).tail = "\n".into(); + } + } + fn write_xml_lines( + lines: impl IntoIterator>, + parent: &mut xml_tree::Element, + trailing_nl: bool, + preceding_nl: bool, + ) { + if preceding_nl { + parent.sub_element("br".into(), []).tail = "\n".into(); + } + let mut first = true; + for line in lines { + let line = line.borrow(); + if first { + first = false; + } else { + parent.sub_element("br".into(), []).tail = "\n".into(); + } + line.write_xml(parent, false); + } + if trailing_nl { + parent.sub_element("br".into(), []).tail = "\n".into(); + } + } +} + +impl fmt::Debug for ParsedTextLine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + element, + regular_min_y, + regular_max_y, + fonts, + chars, + preceding_blank_lines, + } = self; + f.debug_struct("ParsedTextLine") + .field("element", &format_args!("{element}")) + .field("regular_min_y", regular_min_y) + .field("regular_max_y", regular_max_y) + .field("fonts", fonts) + .field("chars", chars) + .field("preceding_blank_lines", preceding_blank_lines) + .finish() + } +} + +impl fmt::Display for ParsedTextLine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for _ in 0..self.preceding_blank_lines { + f.write_str("\n")?; + } + self.element.fmt(f) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum BaselinePos { + Above, + Below, +} + +macro_rules! make_enum_with_values { + ( + $(#[$enum_meta:meta])* + enum $Enum:ident { + $($Variant:ident,)* + } + ) => { + $(#[$enum_meta])* + enum $Enum { + $($Variant,)* + } + + impl $Enum { + const VALUES: &[Self] = &[$(Self::$Variant,)*]; + } + }; +} + +make_enum_with_values! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + enum TextLineFonts { + InsnMnemonicFonts, + InsnHeaderFonts, + InsnBitFieldBitNumberFonts, + InsnBitFieldNameFonts, + InsnBitFieldsAffixTitleFonts, + InsnCodeFonts, + InsnDescFonts, + } +} + +impl TextLineFonts { + fn regular(self) -> &'static [Font] { + match self { + TextLineFonts::InsnMnemonicFonts => KnownFontGroup::InsnDesc.fonts(), + TextLineFonts::InsnHeaderFonts => &[Font::InsnHeader], + TextLineFonts::InsnBitFieldBitNumberFonts => &[Font::InsnDescSmall, Font::TitlePageRev], + TextLineFonts::InsnBitFieldNameFonts => KnownFontGroup::InsnDesc.fonts(), + TextLineFonts::InsnBitFieldsAffixTitleFonts => &[Font::InsnDescSmall], + TextLineFonts::InsnCodeFonts => KnownFontGroup::InsnCode.fonts(), + TextLineFonts::InsnDescFonts => { + static FONTS: OnceLock> = OnceLock::new(); + FONTS.get_or_init(|| { + Box::from_iter( + KnownFontGroup::InsnDesc + .fonts() + .iter() + .cloned() + .chain([Font::InsnDescSmall]), + ) + }) + } + } + } + fn italic(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => { + Some(&[Font::InsnDescItalic, Font::InsnDescSmallItalic]) + } + } + } + fn bold(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => Some(&[Font::InsnDescSmallBold]), + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => Some(&[Font::InsnDescBold, Font::InsnDescSmallBold]), + } + } + fn bold_italic(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => { + Some(&[Font::InsnDescBoldItalic, Font::InsnDescSmallBoldItalic]) + } + } + } + fn subscript(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => Some(&[Font::InsnDescSubscript]), + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => Some(KnownFontGroup::InsnCodeSubscript.fonts()), + TextLineFonts::InsnDescFonts => Some(&[Font::InsnDescSubscript]), + } + } + fn bold_subscript(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => Some(&[Font::InsnDescBoldSubscript]), + } + } + fn italic_subscript(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => Some(&[Font::InsnDescItalicSubscript]), + } + } + fn bold_italic_subscript(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => Some(&[Font::InsnDescBoldItalicSubscript]), + } + } + fn code(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => Some(&[Font::InsnDescCode, Font::InsnExtMnemonic]), + } + } + fn code_subscript(self) -> Option<&'static [Font]> { + match self { + TextLineFonts::InsnMnemonicFonts => None, + TextLineFonts::InsnHeaderFonts => None, + TextLineFonts::InsnBitFieldBitNumberFonts => None, + TextLineFonts::InsnBitFieldNameFonts => None, + TextLineFonts::InsnBitFieldsAffixTitleFonts => None, + TextLineFonts::InsnCodeFonts => None, + TextLineFonts::InsnDescFonts => Some(KnownFontGroup::InsnCodeSubscript.fonts()), + } + } + fn get_fonts( + self, + part_kind: TextLineFontKind, + ) -> Option<(&'static [Font], Option)> { + let fonts = match part_kind { + TextLineFontKind::Regular => self.regular(), + TextLineFontKind::Italic => self.italic()?, + TextLineFontKind::Bold => self.bold()?, + TextLineFontKind::BoldItalic => self.bold_italic()?, + TextLineFontKind::Subscript => self.subscript()?, + TextLineFontKind::Superscript => self.subscript()?, + TextLineFontKind::BoldSubscript => self.bold_subscript()?, + TextLineFontKind::BoldSuperscript => self.bold_subscript()?, + TextLineFontKind::ItalicSubscript => self.italic_subscript()?, + TextLineFontKind::ItalicSuperscript => self.italic_subscript()?, + TextLineFontKind::BoldItalicSubscript => self.bold_italic_subscript()?, + TextLineFontKind::BoldItalicSuperscript => self.bold_italic_subscript()?, + TextLineFontKind::Code => self.code()?, + TextLineFontKind::CodeSubscript => self.code_subscript()?, + TextLineFontKind::CodeSuperscript => self.code_subscript()?, + }; + Some((fonts, part_kind.sub_super().baseline_pos())) + } + fn font_to_kind_map(self) -> &'static HashMap<(Font, Option), TextLineFontKind> { + static MAPS: OnceLock< + HashMap), TextLineFontKind>>, + > = OnceLock::new(); + &MAPS.get_or_init(|| { + Self::VALUES + .iter() + .map(|&this: &TextLineFonts| { + let mut map = HashMap::new(); + for &kind in TextLineFontKind::VALUES { + let Some((fonts, baseline_pos)) = this.get_fonts(kind) else { + continue; + }; + for font in fonts { + let old_kind = map.insert((font.clone(), baseline_pos), kind); + assert!( + old_kind.is_none(), + "duplicate font: kind={kind:?} old_kind={old_kind:?} font={font:?}" + ); + } + } + (this, map) + }) + .collect() + })[&self] + } + fn fonts(self) -> &'static HashSet { + static SETS: OnceLock>> = OnceLock::new(); + &SETS.get_or_init(|| { + Self::VALUES + .iter() + .map(|&this: &TextLineFonts| { + let mut set = HashSet::new(); + for &kind in TextLineFontKind::VALUES { + let Some((fonts, _baseline_pos)) = this.get_fonts(kind) else { + continue; + }; + set.extend(fonts.iter().cloned()); + } + (this, set) + }) + .collect() + })[&self] + } + fn get_kind(self, font: Font, baseline_pos: BaselinePos) -> Option { + let font_to_kind_map = self.font_to_kind_map(); + font_to_kind_map + .get(&(font.clone(), Some(baseline_pos))) + .or_else(|| font_to_kind_map.get(&(font, None))) + .copied() + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum FontVariantCode { + Code, + NotCode, +} + +impl FontVariantCode { + const fn value(self) -> &'static [&'static str] { + match self { + Self::Code => &["code"], + Self::NotCode => &[], + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum FontVariantBold { + Bold, + NotBold, +} + +impl FontVariantBold { + const fn value(self) -> &'static [&'static str] { + match self { + Self::Bold => &["b"], + Self::NotBold => &[], + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum FontVariantItalic { + Italic, + NotItalic, +} + +impl FontVariantItalic { + const fn value(self) -> &'static [&'static str] { + match self { + Self::Italic => &["i"], + Self::NotItalic => &[], + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum FontVariantSubSuper { + NotSubSuper, + Subscript, + Superscript, +} + +impl FontVariantSubSuper { + const fn value(self) -> &'static [&'static str] { + match self { + Self::NotSubSuper => &[], + Self::Subscript => &["sub"], + Self::Superscript => &["sup"], + } + } +} + +impl FontVariantSubSuper { + fn baseline_pos(self) -> Option { + match self { + FontVariantSubSuper::NotSubSuper => None, + FontVariantSubSuper::Subscript => Some(BaselinePos::Below), + FontVariantSubSuper::Superscript => Some(BaselinePos::Above), + } + } +} + +make_enum_with_values! { + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] + enum TextLineFontKind { + Regular, + Subscript, + Superscript, + Italic, + ItalicSubscript, + ItalicSuperscript, + Bold, + BoldSubscript, + BoldSuperscript, + BoldItalic, + BoldItalicSubscript, + BoldItalicSuperscript, + Code, + CodeSubscript, + CodeSuperscript, + } +} + +impl TextLineFontKind { + fn code(self) -> FontVariantCode { + match self { + Self::Regular + | Self::Subscript + | Self::Superscript + | Self::Italic + | Self::ItalicSubscript + | Self::ItalicSuperscript + | Self::Bold + | Self::BoldSubscript + | Self::BoldSuperscript + | Self::BoldItalic + | Self::BoldItalicSubscript + | Self::BoldItalicSuperscript => FontVariantCode::NotCode, + Self::Code | Self::CodeSubscript | Self::CodeSuperscript => FontVariantCode::Code, + } + } + fn bold(self) -> FontVariantBold { + match self { + Self::Regular + | Self::Subscript + | Self::Superscript + | Self::Italic + | Self::ItalicSubscript + | Self::ItalicSuperscript => FontVariantBold::NotBold, + Self::Bold + | Self::BoldSubscript + | Self::BoldSuperscript + | Self::BoldItalic + | Self::BoldItalicSubscript + | Self::BoldItalicSuperscript => FontVariantBold::Bold, + Self::Code | Self::CodeSubscript | Self::CodeSuperscript => FontVariantBold::NotBold, + } + } + fn italic(self) -> FontVariantItalic { + match self { + Self::Regular | Self::Subscript | Self::Superscript => FontVariantItalic::NotItalic, + Self::Italic | Self::ItalicSubscript | Self::ItalicSuperscript => { + FontVariantItalic::Italic + } + Self::Bold | Self::BoldSubscript | Self::BoldSuperscript => { + FontVariantItalic::NotItalic + } + Self::BoldItalic | Self::BoldItalicSubscript | Self::BoldItalicSuperscript => { + FontVariantItalic::Italic + } + Self::Code | Self::CodeSubscript | Self::CodeSuperscript => { + FontVariantItalic::NotItalic + } + } + } + fn sub_super(self) -> FontVariantSubSuper { + match self { + Self::Regular => FontVariantSubSuper::NotSubSuper, + Self::Subscript => FontVariantSubSuper::Subscript, + Self::Superscript => FontVariantSubSuper::Superscript, + Self::Italic => FontVariantSubSuper::NotSubSuper, + Self::ItalicSubscript => FontVariantSubSuper::Subscript, + Self::ItalicSuperscript => FontVariantSubSuper::Superscript, + Self::Bold => FontVariantSubSuper::NotSubSuper, + Self::BoldSubscript => FontVariantSubSuper::Subscript, + Self::BoldSuperscript => FontVariantSubSuper::Superscript, + Self::BoldItalic => FontVariantSubSuper::NotSubSuper, + Self::BoldItalicSubscript => FontVariantSubSuper::Subscript, + Self::BoldItalicSuperscript => FontVariantSubSuper::Superscript, + Self::Code => FontVariantSubSuper::NotSubSuper, + Self::CodeSubscript => FontVariantSubSuper::Subscript, + Self::CodeSuperscript => FontVariantSubSuper::Superscript, + } + } + fn text_line_tags(self) -> impl Clone + Iterator { + self.code() + .value() + .iter() + .copied() + .chain(self.bold().value().iter().copied()) + .chain(self.italic().value().iter().copied()) + .chain(self.sub_super().value().iter().copied()) + } +} + fn main() {} diff --git a/src/xml_tree.rs b/src/xml_tree.rs new file mode 100644 index 0000000..4a0ed52 --- /dev/null +++ b/src/xml_tree.rs @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// See Notices.txt for copyright information + +use quick_xml::{ + Writer, + events::{BytesText, Event}, +}; +use std::fmt; + +struct FmtToIoAdaptor { + writer: W, + partial_char: [u8; 4], + partial_char_len: u8, +} + +impl FmtToIoAdaptor { + fn new(writer: W) -> Self { + Self { + writer, + partial_char: [0; 4], + partial_char_len: 0, + } + } + fn finish(self) -> Result { + let Self { + writer, + partial_char: _, + partial_char_len, + } = self; + if partial_char_len != 0 { + Err(fmt::Error) + } else { + Ok(writer) + } + } + fn write_byte(&mut self, b: u8) -> std::io::Result<()> { + let Self { + writer, + partial_char, + partial_char_len, + } = self; + partial_char[usize::from(*partial_char_len)] = b; + *partial_char_len += 1; + match str::from_utf8(&partial_char[..usize::from(*partial_char_len)]) { + Ok(s) => { + *partial_char_len = 0; + writer.write_str(s).map_err(std::io::Error::other) + } + Err(e) => { + if e.error_len().is_some() { + *partial_char_len = 0; + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + } else { + Ok(()) + } + } + } + } +} + +impl std::io::Write for FmtToIoAdaptor { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + for &b in buf { + self.write_byte(b)?; + } + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] +pub(crate) enum ElementTag { + Comment, + Normal(String), +} + +impl ElementTag { + pub(crate) fn normal(&self) -> Option<&str> { + match self { + ElementTag::Comment => None, + ElementTag::Normal(v) => Some(v), + } + } +} + +impl fmt::Debug for ElementTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Comment => f.write_str(""), + Self::Normal(v) => v.fmt(f), + } + } +} + +/// like python's xml.etree.ElementTree.Element +#[derive(Clone, Debug)] +pub(crate) struct Element { + pub(crate) tag: ElementTag, + pub(crate) attrib: Vec<(String, String)>, + /// text contained in this element but before any children + pub(crate) text: String, + pub(crate) children: Vec, + /// text after the end of this element + pub(crate) tail: String, +} + +/// equivalent to python `xml.etree.ElementTree.tostring(self, encoding="unicode")` +impl fmt::Display for Element { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut writer = Writer::new(FmtToIoAdaptor::new(f)); + fn helper( + element: &Element, + writer: &mut Writer, + ) -> std::io::Result<()> { + let Element { + tag, + attrib, + text, + children, + tail, + } = element; + match tag { + ElementTag::Comment => { + writer.write_event(Event::Comment(BytesText::new(text)))?; + } + ElementTag::Normal(tag) if tag.is_empty() => { + writer.write_event(Event::Text(BytesText::new(text)))?; + } + ElementTag::Normal(tag) => { + let mut element_writer = writer.create_element(tag); + for (name, value) in attrib { + element_writer = + element_writer.with_attribute((name.as_str(), value.as_str())); + } + if text.is_empty() && children.is_empty() { + element_writer.write_empty()?; + } else { + element_writer.write_inner_content(|writer| { + writer.write_event(Event::Text(BytesText::new(text)))?; + for child in children { + helper(child, writer)?; + } + Ok(()) + })?; + } + } + } + writer.write_event(Event::Text(BytesText::new(tail)))?; + Ok(()) + } + helper(self, &mut writer).map_err(|_| fmt::Error)?; + writer.into_inner().finish()?; + Ok(()) + } +} + +impl Element { + /// equivalent to python `"".join(self.itertext())` + pub(crate) fn inner_text(&self) -> String { + let mut retval = String::new(); + fn helper(element: &Element, retval: &mut String) { + let Element { + tag, + attrib: _, + text, + children, + tail: _, + } = element; + let ElementTag::Normal(_) = tag else { + return; + }; + retval.push_str(text); + for child in children { + helper(child, retval); + retval.push_str(&child.tail); + } + } + helper(self, &mut retval); + retval + } + /// equivalent of python's `xml.etree.ElementTree.SubElement()` + pub(crate) fn sub_element( + &mut self, + tag: String, + attrib: impl IntoIterator, + ) -> &mut Self { + self.children.push(Self { + tag: ElementTag::Normal(tag), + attrib: Vec::from_iter(attrib), + text: String::new(), + children: Vec::new(), + tail: String::new(), + }); + self.children.last_mut().expect("just pushed") + } +}