use crate::lex::ast::elements::inlines::{InlineContent, InlineNode, ReferenceInline};
use crate::lex::ast::TextContent;
use lex_extension::wire::{RefKind, WireInline};
pub(crate) fn text_content_to_wire(tc: &TextContent) -> Vec<WireInline> {
if let Some(nodes) = tc.inline_nodes() {
return nodes.iter().map(inline_node_to_wire).collect();
}
let raw = tc.as_string();
if raw.is_empty() {
return Vec::new();
}
vec![WireInline::Text {
text: raw.to_string(),
}]
}
pub(crate) fn inline_nodes_to_wire(nodes: &InlineContent) -> Vec<WireInline> {
nodes.iter().map(inline_node_to_wire).collect()
}
fn inline_node_to_wire(node: &InlineNode) -> WireInline {
match node {
InlineNode::Plain { text, .. } => WireInline::Text { text: text.clone() },
InlineNode::Strong { content, .. } => WireInline::Bold {
children: inline_nodes_to_wire(content),
},
InlineNode::Emphasis { content, .. } => WireInline::Italic {
children: inline_nodes_to_wire(content),
},
InlineNode::Code { text, .. } => WireInline::Code { text: text.clone() },
InlineNode::Math { text, .. } => WireInline::Math { text: text.clone() },
InlineNode::Reference { data, .. } => reference_to_wire(data),
}
}
fn reference_to_wire(data: &ReferenceInline) -> WireInline {
WireInline::Reference {
ref_kind: RefKind::General,
target: data.raw.clone(),
label: None,
}
}
pub(crate) fn text_content_from_wire(inlines: &[WireInline]) -> TextContent {
let mut buf = String::new();
for inline in inlines {
write_inline_source(inline, &mut buf);
}
TextContent::from_string(buf, None)
}
fn write_inline_source(inline: &WireInline, buf: &mut String) {
match inline {
WireInline::Text { text } => buf.push_str(text),
WireInline::Bold { children } => {
buf.push('*');
for c in children {
write_inline_source(c, buf);
}
buf.push('*');
}
WireInline::Italic { children } => {
buf.push('_');
for c in children {
write_inline_source(c, buf);
}
buf.push('_');
}
WireInline::Code { text } => {
buf.push('`');
buf.push_str(text);
buf.push('`');
}
WireInline::Math { text } => {
buf.push('#');
buf.push_str(text);
buf.push('#');
}
WireInline::Reference { target, label, .. } => {
buf.push('[');
buf.push_str(label.as_deref().unwrap_or(target));
buf.push(']');
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lex::ast::elements::inlines::ReferenceInline;
use crate::lex::ast::TextContent;
use crate::lex::inlines::parse_inlines;
use lex_extension::wire::{RefKind, WireInline};
#[test]
fn text_content_to_wire_empty_yields_empty_vec() {
let tc = TextContent::empty();
assert!(text_content_to_wire(&tc).is_empty());
}
#[test]
fn text_content_to_wire_raw_emits_single_text_inline() {
let tc = TextContent::from_string("plain *bold* text".into(), None);
let wire = text_content_to_wire(&tc);
assert_eq!(wire.len(), 1);
match &wire[0] {
WireInline::Text { text } => assert_eq!(text, "plain *bold* text"),
other => panic!("expected Text inline carrying the raw string, got {other:?}"),
}
}
#[test]
fn text_content_to_wire_walks_parsed_inlines() {
let mut tc = TextContent::from_string("hello *loud* world".into(), None);
tc.ensure_inline_parsed();
let wire = text_content_to_wire(&tc);
assert_eq!(wire.len(), 3);
assert!(matches!(&wire[0], WireInline::Text { text } if text == "hello "));
match &wire[1] {
WireInline::Bold { children } => {
assert_eq!(children.len(), 1);
assert!(matches!(&children[0], WireInline::Text { text } if text == "loud"));
}
other => panic!("expected Bold inline, got {other:?}"),
}
assert!(matches!(&wire[2], WireInline::Text { text } if text == " world"));
}
#[test]
fn each_inline_node_variant_maps_to_its_wire_arm() {
let plain = InlineNode::plain("p".into());
assert!(matches!(inline_nodes_to_wire(&vec![plain])[0],
WireInline::Text { ref text } if text == "p"));
let strong = InlineNode::strong(vec![InlineNode::plain("s".into())]);
match &inline_nodes_to_wire(&vec![strong])[0] {
WireInline::Bold { children } => {
assert!(matches!(&children[0], WireInline::Text { text } if text == "s"));
}
other => panic!("Strong → Bold expected, got {other:?}"),
}
let emphasis = InlineNode::emphasis(vec![InlineNode::plain("e".into())]);
match &inline_nodes_to_wire(&vec![emphasis])[0] {
WireInline::Italic { children } => {
assert!(matches!(&children[0], WireInline::Text { text } if text == "e"));
}
other => panic!("Emphasis → Italic expected, got {other:?}"),
}
let code = InlineNode::code("x".into());
assert!(matches!(inline_nodes_to_wire(&vec![code])[0],
WireInline::Code { ref text } if text == "x"));
let math = InlineNode::math("y".into());
assert!(matches!(inline_nodes_to_wire(&vec![math])[0],
WireInline::Math { ref text } if text == "y"));
let reference = InlineNode::reference(ReferenceInline::new("ref".into()));
match &inline_nodes_to_wire(&vec![reference])[0] {
WireInline::Reference {
ref_kind,
target,
label,
} => {
assert_eq!(*ref_kind, RefKind::General);
assert_eq!(target, "ref");
assert!(label.is_none());
}
other => panic!("Reference → Reference expected, got {other:?}"),
}
}
#[test]
fn nested_strong_with_inner_emphasis_preserves_structure() {
let node = InlineNode::strong(vec![
InlineNode::plain("o ".into()),
InlineNode::emphasis(vec![InlineNode::plain("i".into())]),
]);
let wire = inline_nodes_to_wire(&vec![node]);
let WireInline::Bold { children } = &wire[0] else {
panic!("expected Bold");
};
assert_eq!(children.len(), 2);
assert!(matches!(&children[0], WireInline::Text { text } if text == "o "));
let WireInline::Italic { children: inner } = &children[1] else {
panic!("expected nested Italic");
};
assert!(matches!(&inner[0], WireInline::Text { text } if text == "i"));
}
#[test]
fn from_wire_emits_source_form_per_variant() {
let cases: &[(&[WireInline], &str)] = &[
(&[WireInline::Text { text: "raw".into() }], "raw"),
(
&[WireInline::Bold {
children: vec![WireInline::Text { text: "b".into() }],
}],
"*b*",
),
(
&[WireInline::Italic {
children: vec![WireInline::Text { text: "i".into() }],
}],
"_i_",
),
(&[WireInline::Code { text: "c".into() }], "`c`"),
(&[WireInline::Math { text: "m".into() }], "#m#"),
(
&[WireInline::Reference {
ref_kind: RefKind::General,
target: "target".into(),
label: None,
}],
"[target]",
),
(
&[WireInline::Reference {
ref_kind: RefKind::General,
target: "url".into(),
label: Some("see".into()),
}],
"[see]",
),
];
for (inlines, expected) in cases {
let tc = text_content_from_wire(inlines);
assert_eq!(
tc.as_string(),
*expected,
"reverse codec emitted unexpected source for {inlines:?}"
);
}
}
#[test]
fn from_wire_concatenates_multiple_inlines() {
let inlines = vec![
WireInline::Text { text: "a ".into() },
WireInline::Bold {
children: vec![WireInline::Text { text: "b".into() }],
},
WireInline::Text { text: " c".into() },
];
let tc = text_content_from_wire(&inlines);
assert_eq!(tc.as_string(), "a *b* c");
}
#[test]
fn round_trip_preserves_source_for_each_inline_kind() {
let source = "plain *bold* _italic_ `code` #math# [ref]";
let mut tc = TextContent::from_string(source.into(), None);
tc.ensure_inline_parsed();
let wire = text_content_to_wire(&tc);
let back = text_content_from_wire(&wire);
assert_eq!(back.as_string(), source);
}
#[test]
fn inline_nodes_to_wire_handles_parser_output_directly() {
let nodes = parse_inlines("a `code` b");
let wire = inline_nodes_to_wire(&nodes);
assert!(
matches!(wire.last(), Some(WireInline::Text { text }) if text == " b"),
"expected trailing Text inline, got {:?}",
wire.last()
);
assert!(
wire.iter()
.any(|w| matches!(w, WireInline::Code { text } if text == "code")),
"Code inline missing from wire output: {wire:?}",
);
}
}