use std::fmt;
use rd_rds::{Attribute, Attributes, EnvHandle, RObject, RStr, RValue};
use crate::{
RawRdEnvironment, RawRdObject, RawRdReal, RawRdValue, RdAttribute, RdDocument, RdNode, RdTag,
producer,
};
use crate::{RdPath, RdPathSegment};
pub fn lower_r_object(root: &RObject) -> Result<RdDocument, LowerError> {
let mut context = LowerContext::new();
let RValue::List(items) = root.value() else {
return Err(LowerError::RootMustBeList {
location: context.location(None, None),
});
};
let mut nodes = Vec::with_capacity(items.len());
for (index, item) in items.iter().enumerate() {
nodes.push(context.scoped(RdPathSegment::TopLevel(index), |context| {
lower_node(context, item, None)
})?);
}
Ok(RdDocument::new(nodes))
}
fn lower_node(
context: &mut LowerContext,
object: &RObject,
inherited_attribute: Option<&str>,
) -> Result<RdNode, LowerError> {
let tag = rd_tag_string(context, object)?;
let option = lower_option(context, object, tag.as_deref())?;
let node_context = NodeContext {
tag: tag.as_deref(),
value: object.value(),
attributes: object.attributes(),
};
let has_rejected_attributes = object.attributes().iter().any(|attribute| {
matches!(
classify_attribute(&node_context, attribute),
AttributeDisposition::Reject
)
});
let is_leaf_tag = matches!(tag.as_deref(), Some("TEXT" | "RCODE" | "VERB" | "COMMENT"));
if let Some(tag_text) = tag.as_deref() {
if !has_rejected_attributes
&& option.is_none()
&& let Some(leaf) = lower_leaf(
context,
Some(tag_text),
inherited_attribute,
tag_text,
object.value(),
)?
{
return Ok(leaf);
}
if !is_leaf_tag
&& !has_rejected_attributes
&& let RValue::List(children) = object.value()
{
return Ok(RdNode::tagged(
RdTag::from_rd_tag(tag_text),
option,
lower_children(context, inherited_attribute, children)?,
));
}
} else if !has_rejected_attributes
&& option.is_none()
&& let RValue::List(children) = object.value()
{
return Ok(RdNode::group(lower_children(
context,
inherited_attribute,
children,
)?));
}
let (raw_children, payload) =
lower_raw_children(context, tag.as_deref(), inherited_attribute, object.value())?;
let attributes = lower_attributes(context, object, tag.as_deref())?;
Ok(RdNode::Raw(producer::raw_node(
tag,
option,
raw_children,
payload,
attributes,
)))
}
fn lower_children(
context: &mut LowerContext,
inherited_attribute: Option<&str>,
children: &[RObject],
) -> Result<Vec<RdNode>, LowerError> {
let mut lowered = Vec::with_capacity(children.len());
for (index, child) in children.iter().enumerate() {
lowered.push(context.scoped(RdPathSegment::Child(index), |context| {
lower_node(context, child, inherited_attribute)
})?);
}
Ok(lowered)
}
fn lower_option(
context: &mut LowerContext,
object: &RObject,
tag: Option<&str>,
) -> Result<Option<Vec<RdNode>>, LowerError> {
let Some(option) = object.attributes().get("Rd_option") else {
return Ok(None);
};
Ok(Some(context.scoped(RdPathSegment::Option, |context| {
match option.value() {
RValue::List(children) => {
let option_tag = rd_tag_string(context, option)?;
let option_context = NodeContext {
tag: option_tag.as_deref(),
value: option.value(),
attributes: option.attributes(),
};
let has_rejected_attributes = option.attributes().iter().any(|attribute| {
matches!(
classify_attribute(&option_context, attribute),
AttributeDisposition::Reject
)
});
if option_tag.is_some()
|| option.attributes().get("Rd_option").is_some()
|| has_rejected_attributes
{
lower_node(context, option, Some("Rd_option")).map(|node| vec![node])
} else {
lower_children(context, Some("Rd_option"), children)
}
}
RValue::Character(_) => {
lower_raw_children(context, tag, Some("Rd_option"), option.value())
.map(|(children, _)| children)
}
_ => lower_node(context, option, Some("Rd_option")).map(|node| vec![node]),
}
})?))
}
mod attributes;
mod context;
mod error;
mod raw;
mod structured;
use attributes::AttributeDisposition;
use attributes::{classify_attribute, lower_attribute, lower_attributes, valid_rd_tag_value};
use context::{LowerContext, NodeContext};
use raw::lower_raw_children;
use structured::{lower_leaf, rd_tag_string};
pub use error::{LowerError, LowerLocation, StringEncoding};
#[cfg(test)]
#[allow(dead_code, unused_imports)]
mod tests;