use crate::lex::ast::elements::annotation::Annotation as CoreAnnotation;
use crate::lex::ast::elements::blank_line_group::BlankLineGroup;
use crate::lex::ast::elements::content_item::ContentItem;
use crate::lex::ast::elements::data::Data;
use crate::lex::ast::elements::definition::Definition;
use crate::lex::ast::elements::label::Label;
use crate::lex::ast::elements::list::{List, ListItem};
use crate::lex::ast::elements::paragraph::{Paragraph, TextLine};
use crate::lex::ast::elements::parameter::Parameter;
use crate::lex::ast::elements::sequence_marker::SequenceMarker;
use crate::lex::ast::elements::session::Session;
use crate::lex::ast::elements::table::{Table, TableCell, TableCellAlignment, TableRow};
use crate::lex::ast::elements::typed_content::{ContentElement, SessionContent, VerbatimContent};
use crate::lex::ast::elements::verbatim::{Verbatim, VerbatimBlockMode};
use crate::lex::ast::elements::verbatim_line::VerbatimLine;
use crate::lex::ast::TextContent;
use lex_extension::wire::{WireFootnote, WireListItem, WireNode, WireRow, WireTableCell};
use serde_json::Value;
use super::error::FromWireError;
use super::inline::text_content_from_wire;
use super::range::{range_from_wire_with_origin, OriginInterner};
pub fn from_wire_node(node: &WireNode) -> Result<Vec<ContentItem>, FromWireError> {
let mut interner = OriginInterner::new();
match node {
WireNode::Document {
children, origin, ..
} => from_wire_subtree_interned(children, origin.as_deref(), &mut interner),
single => Ok(vec![convert_one(single, None, &mut interner)?]),
}
}
pub fn from_wire_subtree(nodes: &[WireNode]) -> Result<Vec<ContentItem>, FromWireError> {
let mut interner = OriginInterner::new();
from_wire_subtree_interned(nodes, None, &mut interner)
}
fn from_wire_subtree_interned(
nodes: &[WireNode],
inherited_origin: Option<&str>,
interner: &mut OriginInterner,
) -> Result<Vec<ContentItem>, FromWireError> {
nodes
.iter()
.map(|n| convert_one(n, inherited_origin, interner))
.collect()
}
fn convert_one(
node: &WireNode,
inherited_origin: Option<&str>,
interner: &mut OriginInterner,
) -> Result<ContentItem, FromWireError> {
fn effective<'a>(own: Option<&'a str>, inherited: Option<&'a str>) -> Option<&'a str> {
own.or(inherited)
}
match node {
WireNode::Paragraph {
range,
origin,
inlines,
..
} => {
let eff = effective(origin.as_deref(), inherited_origin);
Ok(ContentItem::Paragraph(paragraph_from_wire(
range, eff, inlines, interner,
)))
}
WireNode::Blank { range, origin, .. } => {
let span_lines = range.end.line().saturating_sub(range.start.line());
let count = (span_lines as usize).max(1);
let mut blg = BlankLineGroup::new(count, Vec::new());
let eff = effective(origin.as_deref(), inherited_origin);
blg.location = range_from_wire_with_origin(range, eff, interner);
Ok(ContentItem::BlankLineGroup(blg))
}
WireNode::Annotation {
range,
origin,
label,
params,
body,
..
} => {
let eff = effective(origin.as_deref(), inherited_origin);
Ok(ContentItem::Annotation(annotation_from_wire(
range, eff, label, params, body, interner,
)?))
}
WireNode::Session {
range,
origin,
title,
marker,
children,
..
} => {
let eff = effective(origin.as_deref(), inherited_origin);
Ok(ContentItem::Session(session_from_wire(
range, eff, title, marker, children, interner,
)?))
}
WireNode::Definition {
range,
origin,
subject,
children,
..
} => {
let eff = effective(origin.as_deref(), inherited_origin);
Ok(ContentItem::Definition(definition_from_wire(
range, eff, subject, children, interner,
)?))
}
WireNode::List {
range,
origin,
marker_style,
items,
..
} => {
let eff = effective(origin.as_deref(), inherited_origin);
Ok(ContentItem::List(list_from_wire(
range,
eff,
marker_style,
items,
interner,
)?))
}
WireNode::Table {
range,
origin,
caption,
header_rows,
column_aligns,
rows,
footnotes,
..
} => {
let eff = effective(origin.as_deref(), inherited_origin);
Ok(ContentItem::Table(Box::new(table_from_wire(
range,
eff,
caption,
*header_rows,
column_aligns,
rows,
footnotes,
interner,
)?)))
}
WireNode::Verbatim {
range,
origin,
label,
params,
body_text,
subject,
mode,
..
} => {
let eff = effective(origin.as_deref(), inherited_origin);
Ok(verbatim_from_wire(
range, eff, label, params, body_text, subject, mode, interner,
)?)
}
WireNode::Image {
range,
origin,
src,
alt,
title,
} => {
let eff = effective(origin.as_deref(), inherited_origin);
let mut params = serde_json::Map::new();
params.insert("src".into(), Value::String(src.clone()));
if !alt.is_empty() {
params.insert("alt".into(), Value::String(alt.clone()));
}
if let Some(title) = title {
params.insert("title".into(), Value::String(title.clone()));
}
Ok(verbatim_from_wire(
range,
eff,
"lex.media.image",
&Value::Object(params),
"",
"",
"inflow",
interner,
)?)
}
WireNode::Video {
range,
origin,
src,
title,
poster,
} => {
let eff = effective(origin.as_deref(), inherited_origin);
let mut params = serde_json::Map::new();
params.insert("src".into(), Value::String(src.clone()));
if let Some(title) = title {
params.insert("title".into(), Value::String(title.clone()));
}
if let Some(poster) = poster {
params.insert("poster".into(), Value::String(poster.clone()));
}
Ok(verbatim_from_wire(
range,
eff,
"lex.media.video",
&Value::Object(params),
"",
"",
"inflow",
interner,
)?)
}
WireNode::Audio {
range,
origin,
src,
title,
} => {
let eff = effective(origin.as_deref(), inherited_origin);
let mut params = serde_json::Map::new();
params.insert("src".into(), Value::String(src.clone()));
if let Some(title) = title {
params.insert("title".into(), Value::String(title.clone()));
}
Ok(verbatim_from_wire(
range,
eff,
"lex.media.audio",
&Value::Object(params),
"",
"",
"inflow",
interner,
)?)
}
_ => Err(FromWireError::UnsupportedKind {
kind: kind_name(node).into(),
}),
}
}
fn paragraph_from_wire(
range: &lex_extension::wire::Range,
origin: Option<&str>,
inlines: &[lex_extension::wire::WireInline],
interner: &mut OriginInterner,
) -> Paragraph {
let location = range_from_wire_with_origin(range, origin, interner);
let combined = text_content_from_wire(inlines);
let raw = combined.as_string();
let line_strings: Vec<&str> = if raw.is_empty() {
Vec::new()
} else {
raw.split('\n').collect()
};
let lines: Vec<ContentItem> = line_strings
.into_iter()
.map(|line_str| {
ContentItem::TextLine(TextLine::new(TextContent::from_string(
line_str.to_string(),
None,
)))
})
.collect();
let mut p = if lines.is_empty() {
Paragraph::new(vec![ContentItem::TextLine(TextLine::new(
TextContent::empty(),
))])
} else {
Paragraph::new(lines)
};
p.location = location;
p
}
fn annotation_from_wire(
range: &lex_extension::wire::Range,
origin: Option<&str>,
label: &str,
params: &Value,
body: &Value,
interner: &mut OriginInterner,
) -> Result<CoreAnnotation, FromWireError> {
let parameters = parameters_from_json(params)?;
let label = Label::new(label.to_string());
let data = Data::new(label, parameters);
let children = annotation_body_from_json(body, origin, interner)?;
let mut a = CoreAnnotation::from_data(data, Vec::new());
a.location = range_from_wire_with_origin(range, origin, interner);
if !children.is_empty() {
for child in children {
a.children.as_mut_vec().push(child);
}
}
Ok(a)
}
fn session_from_wire(
range: &lex_extension::wire::Range,
origin: Option<&str>,
title: &str,
marker: &Option<String>,
children: &[WireNode],
interner: &mut OriginInterner,
) -> Result<Session, FromWireError> {
let title_tc = TextContent::from_string(title.to_string(), None);
let typed_children = wire_children_to_session_content(children, origin, interner)?;
let mut s = Session::new(title_tc, typed_children);
s.location = range_from_wire_with_origin(range, origin, interner);
s.marker = marker
.as_deref()
.and_then(|m| SequenceMarker::parse(m, None));
Ok(s)
}
fn definition_from_wire(
range: &lex_extension::wire::Range,
origin: Option<&str>,
subject: &str,
children: &[WireNode],
interner: &mut OriginInterner,
) -> Result<Definition, FromWireError> {
let subject_tc = TextContent::from_string(subject.to_string(), None);
let typed_children = wire_children_to_content_elements(children, origin, interner)?;
let mut d = Definition::new(subject_tc, typed_children);
d.location = range_from_wire_with_origin(range, origin, interner);
Ok(d)
}
fn list_from_wire(
range: &lex_extension::wire::Range,
origin: Option<&str>,
marker_style: &str,
items: &[WireListItem],
interner: &mut OriginInterner,
) -> Result<List, FromWireError> {
let marker = synthetic_marker_for_style(marker_style);
let list_items = items
.iter()
.enumerate()
.map(|(index, item)| list_item_from_wire(item, marker_style, index, origin, interner))
.collect::<Result<Vec<_>, _>>()?;
let mut l = List::new(list_items);
l.location = range_from_wire_with_origin(range, origin, interner);
l.marker = marker;
Ok(l)
}
fn list_item_from_wire(
item: &WireListItem,
marker_style: &str,
index: usize,
parent_origin: Option<&str>,
interner: &mut OriginInterner,
) -> Result<ListItem, FromWireError> {
let combined = text_content_from_wire(&item.inlines);
let raw = combined.as_string();
let text_lines: Vec<TextContent> = if raw.is_empty() {
vec![TextContent::empty()]
} else {
raw.split('\n')
.map(|s| TextContent::from_string(s.to_string(), None))
.collect()
};
let marker_text = synthetic_marker_text(marker_style, index);
let typed_children =
wire_children_to_content_elements(&item.children, parent_origin, interner)?;
let mut li = ListItem::with_text_content(
TextContent::from_string(marker_text, None),
text_lines
.first()
.cloned()
.unwrap_or_else(TextContent::empty),
typed_children,
);
if text_lines.len() > 1 {
li.text = text_lines;
}
li.location = range_from_wire_with_origin(&item.range, parent_origin, interner);
Ok(li)
}
fn synthetic_marker_text(marker_style: &str, index: usize) -> String {
match marker_style {
"dash" => "-".into(),
"numerical" => format!("{}.", index + 1),
"alphabetical" => format!("{}.", char::from(b'a' + ((index % 26) as u8))),
"roman" => format!("{}.", roman_numeral(index + 1)),
_ => "-".into(),
}
}
fn synthetic_marker_for_style(marker_style: &str) -> Option<SequenceMarker> {
let probe = synthetic_marker_text(marker_style, 0);
SequenceMarker::parse(&probe, None)
}
fn roman_numeral(mut n: usize) -> String {
const TABLE: &[(usize, &str)] = &[
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut out = String::new();
for (value, sym) in TABLE {
while n >= *value {
out.push_str(sym);
n -= *value;
}
}
if out.is_empty() {
"I".into()
} else {
out
}
}
#[allow(clippy::too_many_arguments)]
fn table_from_wire(
range: &lex_extension::wire::Range,
origin: Option<&str>,
caption: &str,
header_rows: u32,
column_aligns: &[String],
rows: &[WireRow],
footnotes: &[WireFootnote],
interner: &mut OriginInterner,
) -> Result<Table, FromWireError> {
let column_alignment: Vec<TableCellAlignment> = column_aligns
.iter()
.map(|s| match s.as_str() {
"left" => TableCellAlignment::Left,
"center" => TableCellAlignment::Center,
"right" => TableCellAlignment::Right,
_ => TableCellAlignment::None,
})
.collect();
let header_count = header_rows as usize;
let mut header_vec = Vec::with_capacity(header_count.min(rows.len()));
let mut body_vec = Vec::with_capacity(rows.len().saturating_sub(header_count));
for (i, row) in rows.iter().enumerate() {
let is_header = i < header_count;
let mut col_cursor: usize = 0;
let cells = row
.cells
.iter()
.map(|c| {
let align = column_alignment
.get(col_cursor)
.copied()
.unwrap_or(TableCellAlignment::None);
col_cursor =
col_cursor.saturating_add(usize::try_from(c.colspan.max(1)).unwrap_or(1));
table_cell_from_wire(c, align, is_header)
})
.collect();
let table_row = TableRow::new(cells);
if is_header {
header_vec.push(table_row);
} else {
body_vec.push(table_row);
}
}
let subject = TextContent::from_string(caption.to_string(), None);
let mut t = Table::new(subject, header_vec, body_vec, VerbatimBlockMode::Inflow);
t.location = range_from_wire_with_origin(range, origin, interner);
if !footnotes.is_empty() {
let footnote_items: Vec<ListItem> = footnotes
.iter()
.map(|f| {
let combined = text_content_from_wire(&f.inlines);
ListItem::with_text_content(
TextContent::from_string(f.marker.clone(), None),
combined,
Vec::new(),
)
})
.collect();
t.footnotes = Some(Box::new(List::new(footnote_items)));
}
Ok(t)
}
fn table_cell_from_wire(
cell: &WireTableCell,
align: TableCellAlignment,
header: bool,
) -> TableCell {
let content = text_content_from_wire(&cell.inlines);
TableCell::new(content)
.with_span(cell.colspan as usize, cell.rowspan as usize)
.with_align(align)
.with_header(header)
}
#[allow(clippy::too_many_arguments)]
fn verbatim_from_wire(
range: &lex_extension::wire::Range,
origin: Option<&str>,
label: &str,
params: &Value,
body_text: &str,
subject: &str,
mode: &str,
interner: &mut OriginInterner,
) -> Result<ContentItem, FromWireError> {
if label == "lex.internal.unsupported.unknown" {
return Err(FromWireError::UnsupportedKind {
kind: "unknown".into(),
});
}
if let Some(stripped) = label.strip_prefix("lex.internal.unsupported.") {
return Err(FromWireError::UnsupportedKind {
kind: stripped.to_string(),
});
}
if label.is_empty() {
let mut vl =
VerbatimLine::from_text_content(TextContent::from_string(body_text.to_string(), None));
vl.location = range_from_wire_with_origin(range, origin, interner);
return Ok(ContentItem::VerbatimLine(vl));
}
let parameters = parameters_from_json(params)?;
let closing_data = Data::new(Label::new(label.to_string()), parameters);
let typed_lines: Vec<VerbatimContent> = if body_text.is_empty() {
Vec::new()
} else {
body_text
.split('\n')
.map(|line| {
VerbatimContent::VerbatimLine(VerbatimLine::from_text_content(
TextContent::from_string(line.to_string(), None),
))
})
.collect()
};
let subject_tc = if subject.is_empty() {
TextContent::empty()
} else {
TextContent::from_string(subject.to_string(), None)
};
let block_mode = parse_verbatim_mode(mode);
let mut v = Verbatim::new(subject_tc, typed_lines, closing_data, block_mode);
v.location = range_from_wire_with_origin(range, origin, interner);
Ok(ContentItem::VerbatimBlock(Box::new(v)))
}
fn parse_verbatim_mode(mode: &str) -> VerbatimBlockMode {
match mode {
"fullwidth" => VerbatimBlockMode::Fullwidth,
_ => VerbatimBlockMode::Inflow,
}
}
fn parameters_from_json(params: &Value) -> Result<Vec<Parameter>, FromWireError> {
let obj = params
.as_object()
.ok_or_else(|| FromWireError::MalformedField {
field: "params",
detail: "expected JSON object".into(),
})?;
let mut out = Vec::with_capacity(obj.len());
for (k, v) in obj {
let value_str = match v {
Value::String(s) => s.clone(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::Null => String::new(),
other => other.to_string(),
};
out.push(Parameter {
key: k.clone(),
value: value_str,
location: crate::lex::ast::range::Range::new(
0..0,
crate::lex::ast::range::Position::new(0, 0),
crate::lex::ast::range::Position::new(0, 0),
),
});
}
Ok(out)
}
fn annotation_body_from_json(
body: &Value,
inherited_origin: Option<&str>,
interner: &mut OriginInterner,
) -> Result<Vec<ContentItem>, FromWireError> {
match body {
Value::Null => Ok(Vec::new()),
Value::String(text) => {
let line = TextLine::new(TextContent::from_string(text.clone(), None));
let para = Paragraph::new(vec![ContentItem::TextLine(line)]);
Ok(vec![ContentItem::Paragraph(para)])
}
Value::Object(obj) => {
let kind = obj.get("kind").and_then(|v| v.as_str());
if kind != Some("block") {
return Err(FromWireError::MalformedField {
field: "body.kind",
detail: format!("expected \"block\", got {kind:?}"),
});
}
let children: Vec<WireNode> = match obj.get("children") {
Some(arr) => serde_json::from_value(arr.clone())
.map_err(|e| FromWireError::DeserialisationFailed(e.to_string()))?,
None => Vec::new(),
};
from_wire_subtree_interned(&children, inherited_origin, interner)
}
_ => Err(FromWireError::MalformedField {
field: "body",
detail: "expected null, string, or object".into(),
}),
}
}
fn wire_children_to_session_content(
children: &[WireNode],
inherited_origin: Option<&str>,
interner: &mut OriginInterner,
) -> Result<Vec<SessionContent>, FromWireError> {
let items = from_wire_subtree_interned(children, inherited_origin, interner)?;
Ok(items.into_iter().map(SessionContent::from).collect())
}
fn wire_children_to_content_elements(
children: &[WireNode],
inherited_origin: Option<&str>,
interner: &mut OriginInterner,
) -> Result<Vec<ContentElement>, FromWireError> {
let items = from_wire_subtree_interned(children, inherited_origin, interner)?;
items
.into_iter()
.map(|item| {
ContentElement::try_from(item).map_err(|e| FromWireError::MalformedField {
field: "children",
detail: e.to_string(),
})
})
.collect()
}
fn kind_name(node: &WireNode) -> &'static str {
match node {
WireNode::Document { .. } => "document",
WireNode::Session { .. } => "session",
WireNode::Definition { .. } => "definition",
WireNode::Paragraph { .. } => "paragraph",
WireNode::List { .. } => "list",
WireNode::Verbatim { .. } => "verbatim",
WireNode::Table { .. } => "table",
WireNode::Image { .. } => "image",
WireNode::Video { .. } => "video",
WireNode::Audio { .. } => "audio",
WireNode::Annotation { .. } => "annotation",
WireNode::Blank { .. } => "blank",
_ => "unknown",
}
}