use quarb::{AstAdapter, NodeId, Value};
pub mod render;
pub use render::{Render, render_node, render_nodes};
#[derive(Debug, Clone, PartialEq)]
pub enum Block {
Heading { level: u8, lemma: String },
Paragraph { text: String },
Text { text: String },
Open { kind: Container, lemma: Option<String> },
Close { hypograph: Option<String> },
Verbatim { lang: Option<String>, text: String },
Table {
lemma: Option<String>,
headers: Option<Vec<String>>,
rows: Vec<Vec<String>>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Container {
Blockquote,
UnorderedList,
OrderedList { start: i64 },
Item,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Document,
Section,
Paragraph,
Blockquote,
UnorderedList,
OrderedList,
UnorderedItem,
OrderedItem,
Verbatim,
}
impl Kind {
fn name(self) -> Option<&'static str> {
Some(match self {
Kind::Document => return None,
Kind::Section => "section",
Kind::Paragraph => "paragraph",
Kind::Blockquote => "blockquote",
Kind::UnorderedList => "unordered-list",
Kind::OrderedList => "ordered-list",
Kind::UnorderedItem => "unordered-item",
Kind::OrderedItem => "ordered-item",
Kind::Verbatim => "verbatim",
})
}
}
struct Node {
kind: Kind,
lemma: Option<String>,
hypograph: Option<String>,
taxis: Option<i64>,
level: Option<u8>,
lang: Option<String>,
start: i64,
text: String,
prose: String,
table: bool,
parent: Option<NodeId>,
children: Vec<NodeId>,
}
impl Node {
fn new(kind: Kind, parent: Option<NodeId>) -> Self {
Node {
kind,
lemma: None,
hypograph: None,
taxis: None,
level: None,
lang: None,
start: 1,
text: String::new(),
prose: String::new(),
table: false,
parent,
children: Vec::new(),
}
}
}
pub fn normalize_ws(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub struct TextModel {
nodes: Vec<Node>,
root: NodeId,
}
impl TextModel {
pub fn build(blocks: Vec<Block>) -> Self {
let mut nodes = vec![Node::new(Kind::Document, None)];
let root = NodeId(0);
let mut sections: Vec<NodeId> = Vec::new();
let mut containers: Vec<NodeId> = Vec::new();
for block in blocks {
match block {
Block::Heading { level, lemma } => {
let lemma = normalize_ws(&lemma);
if !containers.is_empty() {
if !lemma.is_empty() {
let parent = *containers.last().unwrap();
let id = push(&mut nodes, Kind::Paragraph, parent);
nodes[id.0 as usize].text = lemma;
}
continue;
}
while let Some(&open) = sections.last() {
if nodes[open.0 as usize].level >= Some(level) {
sections.pop();
} else {
break;
}
}
let parent = sections.last().copied().unwrap_or(root);
let id = push(&mut nodes, Kind::Section, parent);
let n = &mut nodes[id.0 as usize];
n.lemma = Some(lemma);
n.level = Some(level);
sections.push(id);
}
Block::Paragraph { text } => {
let text = normalize_ws(&text);
if text.is_empty() {
continue;
}
let parent = cursor(§ions, &containers, root);
let id = push(&mut nodes, Kind::Paragraph, parent);
nodes[id.0 as usize].text = text;
}
Block::Text { text } => {
let text = normalize_ws(&text);
if text.is_empty() {
continue;
}
match containers.last() {
Some(&open) => {
let own = &mut nodes[open.0 as usize].text;
if !own.is_empty() {
own.push(' ');
}
own.push_str(&text);
}
None => {
let parent = sections.last().copied().unwrap_or(root);
let id = push(&mut nodes, Kind::Paragraph, parent);
nodes[id.0 as usize].text = text;
}
}
}
Block::Open { kind, lemma } => {
let parent = cursor(§ions, &containers, root);
let (nkind, start) = match kind {
Container::Blockquote => (Kind::Blockquote, None),
Container::UnorderedList => (Kind::UnorderedList, None),
Container::OrderedList { start } => (Kind::OrderedList, Some(start)),
Container::Item => (
match nodes[parent.0 as usize].kind {
Kind::OrderedList => Kind::OrderedItem,
_ => Kind::UnorderedItem,
},
None,
),
};
let id = push(&mut nodes, nkind, parent);
nodes[id.0 as usize].lemma =
lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
if let Some(start) = start {
nodes[id.0 as usize].start = start;
}
if nkind == Kind::OrderedItem {
let nth = nodes[parent.0 as usize]
.children
.iter()
.filter(|&&c| nodes[c.0 as usize].kind == Kind::OrderedItem)
.count() as i64;
let start = nodes[parent.0 as usize].start;
nodes[id.0 as usize].taxis = Some(start + nth - 1);
}
containers.push(id);
}
Block::Close { hypograph } => {
if let Some(open) = containers.pop() {
nodes[open.0 as usize].hypograph =
hypograph.map(|h| normalize_ws(&h)).filter(|h| !h.is_empty());
}
}
Block::Verbatim { lang, text } => {
let parent = cursor(§ions, &containers, root);
let id = push(&mut nodes, Kind::Verbatim, parent);
let n = &mut nodes[id.0 as usize];
n.lang = lang.filter(|l| !l.is_empty());
n.text = text;
}
Block::Table {
lemma,
headers,
rows,
} => {
let parent = cursor(§ions, &containers, root);
lower_table(&mut nodes, parent, lemma, headers, rows);
}
}
}
flatten_prose(&mut nodes);
TextModel { nodes, root }
}
pub fn parse_plain(text: &str) -> Self {
let mut blocks = Vec::new();
let mut para: Vec<&str> = Vec::new();
for line in text.lines() {
if line.trim().is_empty() {
if !para.is_empty() {
blocks.push(Block::Paragraph {
text: para.join(" "),
});
para.clear();
}
} else {
para.push(line);
}
}
if !para.is_empty() {
blocks.push(Block::Paragraph {
text: para.join(" "),
});
}
Self::build(blocks)
}
pub fn locator(&self, node: NodeId) -> String {
let mut segments = Vec::new();
let mut cur = Some(node);
while let Some(id) = cur {
let n = &self.nodes[id.0 as usize];
if let Some(name) = n.kind.name() {
segments.push(self.segment(id, name));
}
cur = n.parent;
}
segments.reverse();
format!("/{}", segments.join("/"))
}
fn segment(&self, node: NodeId, name: &str) -> String {
let Some(parent) = self.nodes[node.0 as usize].parent else {
return name.to_string();
};
let siblings = &self.nodes[parent.0 as usize].children;
let same_name: Vec<NodeId> = siblings
.iter()
.copied()
.filter(|&s| self.nodes[s.0 as usize].kind == self.nodes[node.0 as usize].kind)
.collect();
if same_name.len() > 1 {
let n = same_name.iter().position(|&s| s == node).unwrap() + 1;
format!("{name}[{n}]")
} else {
name.to_string()
}
}
}
fn cursor(sections: &[NodeId], containers: &[NodeId], root: NodeId) -> NodeId {
containers
.last()
.or(sections.last())
.copied()
.unwrap_or(root)
}
fn push(nodes: &mut Vec<Node>, kind: Kind, parent: NodeId) -> NodeId {
let id = NodeId(nodes.len() as u64);
nodes.push(Node::new(kind, Some(parent)));
nodes[parent.0 as usize].children.push(id);
id
}
fn lower_table(
nodes: &mut Vec<Node>,
parent: NodeId,
lemma: Option<String>,
headers: Option<Vec<String>>,
rows: Vec<Vec<String>>,
) {
let list = push(nodes, Kind::OrderedList, parent);
{
let n = &mut nodes[list.0 as usize];
n.table = true;
n.lemma = lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
}
for (i, row) in rows.into_iter().enumerate() {
let item = push(nodes, Kind::OrderedItem, list);
nodes[item.0 as usize].taxis = Some(i as i64 + 1);
let cells = push(nodes, Kind::UnorderedList, item);
for (j, cell) in row.into_iter().enumerate() {
let value = normalize_ws(&cell);
if value.is_empty() {
continue;
}
let header = headers
.as_ref()
.and_then(|h| h.get(j))
.map(|h| normalize_ws(h))
.filter(|h| !h.is_empty());
let text = match header {
Some(h) => format!("{h}: {value}"),
None => value,
};
let cell_item = push(nodes, Kind::UnorderedItem, cells);
nodes[cell_item.0 as usize].text = text;
}
}
}
fn flatten_prose(nodes: &mut [Node]) {
for i in (0..nodes.len()).rev() {
let mut parts: Vec<String> = Vec::new();
if let Some(lemma) = &nodes[i].lemma
&& !lemma.is_empty()
{
parts.push(lemma.clone());
}
if !nodes[i].text.is_empty() {
parts.push(nodes[i].text.clone());
}
for &child in nodes[i].children.clone().iter() {
let prose = &nodes[child.0 as usize].prose;
if !prose.is_empty() {
parts.push(prose.clone());
}
}
if let Some(hypograph) = &nodes[i].hypograph
&& !hypograph.is_empty()
{
parts.push(hypograph.clone());
}
nodes[i].prose = parts.join("\n");
}
}
impl AstAdapter for TextModel {
fn root(&self) -> NodeId {
self.root
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.nodes[node.0 as usize].children.clone()
}
fn name(&self, node: NodeId) -> Option<String> {
self.nodes[node.0 as usize].kind.name().map(str::to_string)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.nodes[node.0 as usize].parent
}
fn traits(&self, node: NodeId) -> Vec<String> {
let n = &self.nodes[node.0 as usize];
let mut out = Vec::new();
if n.kind != Kind::Document {
out.push("block".to_string());
}
if n.table {
out.push("table".to_string());
}
out
}
fn property(&self, node: NodeId, name: &str) -> Option<Value> {
let n = &self.nodes[node.0 as usize];
match name {
"lemma" => n.lemma.clone().map(Value::Str),
"hypograph" => n.hypograph.clone().map(Value::Str),
"taxis" => n.taxis.map(Value::Int),
"text" => Some(Value::Str(n.prose.clone())),
_ => None,
}
}
fn default_value(&self, node: NodeId) -> Option<Value> {
Some(Value::Str(self.nodes[node.0 as usize].prose.clone()))
}
fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
let n = &self.nodes[node.0 as usize];
match key {
"level" => n.level.map(|l| Value::Int(l as i64)),
"lang" => n.lang.clone().map(Value::Str),
_ => None,
}
}
}