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 },
NoteRef {
onym: String,
family: Option<NoteFamily>,
margin: bool,
},
IndexMark { term: String },
Ref {
target: String,
text: Option<String>,
internal: bool,
},
Anchor { onym: String },
Cite { target: String },
Bib {
key: String,
text: String,
fields: Vec<(String, String)>,
genus: Option<String>,
},
Label { onym: String },
Verse {
lemma: Option<String>,
strophes: Vec<Vec<String>>,
hypograph: Option<String>,
},
Table {
lemma: Option<String>,
headers: Option<Vec<String>>,
rows: Vec<Vec<Cell>>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
pub label: Option<String>,
pub text: String,
}
impl From<&str> for Cell {
fn from(text: &str) -> Self {
Cell {
label: None,
text: text.to_string(),
}
}
}
impl From<String> for Cell {
fn from(text: String) -> Self {
Cell { label: None, text }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Container {
Blockquote,
UnorderedList,
OrderedList { start: i64 },
Item,
Note {
onym: String,
family: NoteFamily,
margin: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NoteFamily {
Footnote,
Endnote,
Aside,
}
impl NoteFamily {
fn kind(self) -> Kind {
match self {
NoteFamily::Footnote => Kind::Footnote,
NoteFamily::Endnote => Kind::Endnote,
NoteFamily::Aside => Kind::Aside,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Kind {
Document,
Section,
Paragraph,
Blockquote,
UnorderedList,
OrderedList,
UnorderedItem,
OrderedItem,
Verbatim,
Footnote,
Endnote,
Aside,
IndexMark,
Ref,
Cit,
Bib,
Anchor,
Verse,
Strophe,
Stichos,
}
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::Footnote => "footnote",
Kind::Endnote => "endnote",
Kind::Aside => "aside",
Kind::IndexMark => "index-mark",
Kind::Ref => "ref",
Kind::Cit => "cit",
Kind::Bib => "bib",
Kind::Anchor => "anchor",
Kind::Verse => "verse",
Kind::Strophe => "strophe",
Kind::Stichos => "stichos",
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,
row: bool,
cell: bool,
onym: Option<String>,
deixis: bool,
dangling: bool,
family_open: bool,
margin: bool,
note_edge: Option<NodeId>,
cites: Vec<NodeId>,
target: Option<String>,
internal: bool,
fields: Vec<(String, String)>,
genus: Option<String>,
ref_edge: Option<NodeId>,
ref_cites: Vec<NodeId>,
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,
row: false,
cell: false,
onym: None,
deixis: false,
dangling: false,
family_open: false,
margin: false,
note_edge: None,
cites: Vec::new(),
target: None,
internal: false,
fields: Vec::new(),
genus: None,
ref_edge: None,
ref_cites: Vec::new(),
parent,
children: Vec::new(),
}
}
}
fn resolve_refs(nodes: &mut [Node]) -> std::collections::HashMap<String, NodeId> {
let mut onyms: std::collections::HashMap<String, NodeId> =
std::collections::HashMap::new();
let mut bibs: std::collections::HashMap<String, NodeId> =
std::collections::HashMap::new();
for (i, n) in nodes.iter().enumerate() {
if n.deixis || matches!(n.kind, Kind::IndexMark | Kind::Ref | Kind::Cit) {
continue;
}
if n.kind == Kind::Bib {
if let Some(k) = &n.onym {
bibs.entry(k.clone()).or_insert(NodeId(i as u64));
}
continue;
}
let bearer = n.kind == Kind::Anchor
|| (!matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
&& n.onym.is_some());
if bearer && let Some(o) = &n.onym {
onyms.entry(o.clone()).or_insert(NodeId(i as u64));
}
}
for i in 0..nodes.len() {
let (map, key) = match nodes[i].kind {
Kind::Ref if nodes[i].internal => (
&onyms,
nodes[i]
.target
.as_deref()
.map(|t| t.trim_start_matches('#').to_string())
.unwrap_or_default(),
),
Kind::Cit => (
&bibs,
nodes[i].target.clone().unwrap_or_default(),
),
_ => continue,
};
match map.get(&key) {
Some(&bearer) => {
nodes[i].ref_edge = Some(bearer);
let me = NodeId(i as u64);
nodes[bearer.0 as usize].ref_cites.push(me);
}
None => nodes[i].dangling = true,
}
}
onyms
}
pub fn normalize_ws(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub struct TextModel {
nodes: Vec<Node>,
root: NodeId,
onyms: std::collections::HashMap<String, NodeId>,
document_url: Option<url::Url>,
bib_aliases: std::collections::HashMap<String, String>,
}
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();
let mut last_flow: Option<NodeId> = None;
let mut last_block: Option<NodeId> = None;
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);
last_flow = Some(id);
last_block = Some(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;
last_flow = Some(id);
last_block = Some(id);
}
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);
last_flow = Some(open);
}
None => {
let parent = sections.last().copied().unwrap_or(root);
let id = push(&mut nodes, Kind::Paragraph, parent);
nodes[id.0 as usize].text = text;
last_flow = Some(id);
}
}
}
Block::Open { kind, lemma } => {
let kind = match kind {
Container::Note { onym, family, margin } => {
let id = push(&mut nodes, family.kind(), root);
let n = &mut nodes[id.0 as usize];
n.onym = Some(onym.trim().to_string()).filter(|o| !o.is_empty());
n.margin = margin;
containers.push(id);
last_flow = Some(id);
continue;
}
other => other,
};
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,
),
Container::Note { .. } => unreachable!("handled above"),
};
let id = push(&mut nodes, nkind, parent);
last_block = Some(id);
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::NoteRef { onym, family, margin } => {
let parent = last_flow.unwrap_or(root);
let kind = family.map(NoteFamily::kind).unwrap_or(Kind::Footnote);
let id = push(&mut nodes, kind, parent);
let n = &mut nodes[id.0 as usize];
n.deixis = true;
n.family_open = family.is_none();
n.margin = margin;
n.onym = Some(onym.trim().to_string()).filter(|o| !o.is_empty());
}
Block::Ref { target, text, internal } => {
let parent = last_flow.unwrap_or(root);
let id = push(&mut nodes, Kind::Ref, parent);
let n = &mut nodes[id.0 as usize];
n.internal = internal;
n.target = Some(target.trim().to_string()).filter(|t| !t.is_empty());
n.text = text.map(|t| normalize_ws(&t)).unwrap_or_default();
}
Block::Cite { target } => {
let parent = last_flow.unwrap_or(root);
let id = push(&mut nodes, Kind::Cit, parent);
let n = &mut nodes[id.0 as usize];
n.internal = true;
n.target = Some(target.trim().to_string()).filter(|t| !t.is_empty());
}
Block::Bib {
key,
text,
fields,
genus,
} => {
let parent = cursor(§ions, &containers, root);
let id = push(&mut nodes, Kind::Bib, parent);
last_block = Some(id);
let n = &mut nodes[id.0 as usize];
n.onym = Some(key.trim().to_string()).filter(|k| !k.is_empty());
n.text = normalize_ws(&text);
n.fields = fields
.into_iter()
.map(|(k, v)| (k.trim().to_string(), normalize_ws(&v)))
.filter(|(k, v)| !k.is_empty() && !v.is_empty())
.collect();
n.genus = genus.map(|g| g.trim().to_string()).filter(|g| !g.is_empty());
}
Block::Anchor { onym } => {
let parent = last_flow.unwrap_or(root);
let id = push(&mut nodes, Kind::Anchor, parent);
nodes[id.0 as usize].onym =
Some(onym.trim().to_string()).filter(|o| !o.is_empty());
}
Block::Label { onym } => {
let onym = onym.trim().to_string();
if onym.is_empty() {
continue;
}
let free = last_block
.map(|b| nodes[b.0 as usize].onym.is_none())
.unwrap_or(false);
match (last_block, free) {
(Some(b), true) => {
nodes[b.0 as usize].onym = Some(onym);
}
_ => {
let parent = last_flow.unwrap_or(root);
let id = push(&mut nodes, Kind::Anchor, parent);
nodes[id.0 as usize].onym = Some(onym);
}
}
}
Block::IndexMark { term } => {
let parent = last_flow.unwrap_or(root);
let id = push(&mut nodes, Kind::IndexMark, parent);
nodes[id.0 as usize].onym =
Some(quarb_term(&term)).filter(|t| !t.is_empty());
}
Block::Verbatim { lang, text } => {
let parent = cursor(§ions, &containers, root);
let id = push(&mut nodes, Kind::Verbatim, parent);
last_block = Some(id);
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);
}
Block::Verse {
lemma,
strophes,
hypograph,
} => {
let parent = cursor(§ions, &containers, root);
let id = lower_verse(&mut nodes, parent, lemma, strophes, hypograph);
last_flow = Some(id);
last_block = Some(id);
}
}
}
resolve_notes(&mut nodes);
let onyms = resolve_refs(&mut nodes);
flatten_prose(&mut nodes);
TextModel {
nodes,
root,
onyms,
document_url: None,
bib_aliases: Default::default(),
}
}
pub fn set_bib_aliases(&mut self, map: std::collections::HashMap<String, String>) {
self.bib_aliases = map;
}
pub fn set_document_url(&mut self, url: &str) {
self.document_url = url::Url::parse(url).ok();
}
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 resolve_notes(nodes: &mut [Node]) {
let mut bodies: std::collections::HashMap<(Kind, String), NodeId> = Default::default();
for (i, n) in nodes.iter().enumerate() {
if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
&& !n.deixis
&& let Some(o) = &n.onym
{
bodies.entry((n.kind, o.clone())).or_insert(NodeId(i as u64));
}
}
for i in 0..nodes.len() {
if !matches!(nodes[i].kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
|| !nodes[i].deixis
{
continue;
}
let Some(onym) = nodes[i].onym.clone() else {
nodes[i].dangling = true;
continue;
};
let hit = if nodes[i].family_open {
[Kind::Footnote, Kind::Endnote, Kind::Aside]
.into_iter()
.find_map(|k| bodies.get(&(k, onym.clone())).map(|b| (k, *b)))
} else {
bodies.get(&(nodes[i].kind, onym)).map(|b| (nodes[i].kind, *b))
};
match hit {
Some((family, body)) => {
nodes[i].kind = family;
nodes[i].note_edge = Some(body);
let callout = NodeId(i as u64);
nodes[body.0 as usize].cites.push(callout);
}
None => nodes[i].dangling = true,
}
}
}
fn quarb_term(term: &str) -> String {
normalize_ws(term.split('|').next().unwrap_or(term))
}
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<Cell>>,
) {
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);
nodes[item.0 as usize].row = true;
let cells = push(nodes, Kind::UnorderedList, item);
for (j, cell) in row.into_iter().enumerate() {
let value = normalize_ws(&cell.text);
if value.is_empty() {
continue;
}
let label = cell
.label
.as_deref()
.or_else(|| headers.as_ref().and_then(|h| h.get(j)).map(|h| h.as_str()))
.map(normalize_ws)
.filter(|h| !h.is_empty());
let cell_item = push(nodes, Kind::UnorderedItem, cells);
let n = &mut nodes[cell_item.0 as usize];
n.cell = true;
n.lemma = label;
n.text = value;
}
}
}
fn lower_verse(
nodes: &mut Vec<Node>,
parent: NodeId,
lemma: Option<String>,
strophes: Vec<Vec<String>>,
hypograph: Option<String>,
) -> NodeId {
let verse = push(nodes, Kind::Verse, parent);
{
let n = &mut nodes[verse.0 as usize];
n.lemma = lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
n.hypograph = hypograph.map(|h| normalize_ws(&h)).filter(|h| !h.is_empty());
}
let mut line_no = 0i64;
for (i, strophe) in strophes.into_iter().enumerate() {
let sid = push(nodes, Kind::Strophe, verse);
nodes[sid.0 as usize].taxis = Some(i as i64 + 1);
for line in strophe {
let line = line.trim_end().to_string();
if line.trim().is_empty() {
continue;
}
line_no += 1;
let lid = push(nodes, Kind::Stichos, sid);
let n = &mut nodes[lid.0 as usize];
n.taxis = Some(line_no);
n.text = line;
}
}
verse
}
fn flatten_prose(nodes: &mut [Node]) {
for i in (0..nodes.len()).rev() {
let inline_lemma = matches!(
nodes[i].kind,
Kind::UnorderedItem | Kind::OrderedItem
);
let mut lemma_part: Option<String> = None;
let mut parts: Vec<String> = Vec::new();
if let Some(lemma) = &nodes[i].lemma
&& !lemma.is_empty()
{
if inline_lemma {
lemma_part = Some(lemma.clone());
} else {
parts.push(lemma.clone());
}
}
if !nodes[i].text.is_empty() {
parts.push(nodes[i].text.clone());
}
let mut child_parts: Vec<String> = Vec::new();
for &child in nodes[i].children.clone().iter() {
if matches!(
nodes[child.0 as usize].kind,
Kind::Ref | Kind::Cit | Kind::Anchor
) {
continue;
}
let prose = &nodes[child.0 as usize].prose;
if !prose.is_empty() {
child_parts.push(prose.clone());
}
}
if nodes[i].kind == Kind::Verse {
let joined = child_parts.join("\n\n");
if !joined.is_empty() {
parts.push(joined);
}
} else {
parts.extend(child_parts);
}
if let Some(hypograph) = &nodes[i].hypograph
&& !hypograph.is_empty()
{
parts.push(hypograph.clone());
}
let mut prose = parts.join("\n");
if let Some(lemma) = lemma_part {
prose = if prose.is_empty() {
lemma
} else {
format!("{lemma}: {prose}")
};
}
nodes[i].prose = prose;
}
}
impl TextModel {
fn grammata(&self, node: NodeId) -> String {
let n = &self.nodes[node.0 as usize];
let mut s = n.prose.as_str();
if let Some(lemma) = &n.lemma
&& !lemma.is_empty()
&& let Some(rest) = s.strip_prefix(lemma.as_str())
{
s = rest
.strip_prefix(": ")
.or_else(|| rest.strip_prefix('\n'))
.unwrap_or(rest);
}
if let Some(h) = &n.hypograph
&& !h.is_empty()
&& let Some(rest) = s.strip_suffix(h.as_str())
{
s = rest.strip_suffix('\n').unwrap_or(rest);
}
s.trim_end().to_string()
}
}
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
&& !n.deixis
&& !matches!(
n.kind,
Kind::IndexMark
| Kind::Ref
| Kind::Cit
| Kind::Anchor
| Kind::Strophe
| Kind::Stichos
)
{
out.push("block".to_string());
}
if matches!(n.kind, Kind::Ref | Kind::Cit) && n.dangling {
out.push("dangling".to_string());
}
if n.kind == Kind::Bib && let Some(g) = &n.genus {
out.push(g.clone());
}
if n.kind == Kind::Anchor
|| (n.onym.is_some()
&& !n.deixis
&& !matches!(
n.kind,
Kind::Footnote
| Kind::Endnote
| Kind::Aside
| Kind::IndexMark
| Kind::Ref
| Kind::Cit
| Kind::Bib
))
{
out.push("target".to_string());
}
if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) {
if n.deixis {
out.push("deixis".to_string());
} else if n.kind != Kind::Aside {
out.push("note".to_string());
}
if n.dangling {
out.push("dangling".to_string());
}
}
if n.table {
out.push("table".to_string());
}
if n.row {
out.push("row".to_string());
}
if n.cell {
out.push("cell".to_string());
}
out
}
fn property(&self, node: NodeId, name: &str) -> Option<Value> {
let n = &self.nodes[node.0 as usize];
if n.kind == Kind::Bib && !n.fields.is_empty() {
let canon = self
.bib_aliases
.get(&name.to_lowercase())
.map(String::as_str)
.unwrap_or(name);
if let Some((_, v)) = n.fields.iter().find(|(k, _)| k == canon) {
return Some(Value::Str(v.clone()));
}
}
match name {
"lemma" | "title" => n.lemma.clone().map(Value::Str),
"onym" => n.onym.clone().map(Value::Str),
"term" if n.kind == Kind::IndexMark => n.onym.clone().map(Value::Str),
"target" if matches!(n.kind, Kind::Ref | Kind::Cit) => {
n.target.clone().map(Value::Str)
}
_ if n.kind == Kind::Bib => {
let canon = self
.bib_aliases
.get(&name.to_lowercase())
.map(String::as_str)
.unwrap_or(name);
n.fields
.iter()
.find(|(k, _)| k == canon)
.map(|(_, v)| Value::Str(v.clone()))
}
"hypograph" | "attribution" => n.hypograph.clone().map(Value::Str),
"taxis" | "ord" => n.taxis.map(Value::Int),
"grammata" | "body" => {
let g = self.grammata(node);
if g.is_empty() { None } else { Some(Value::Str(g)) }
}
"text" => Some(Value::Str(n.prose.clone())),
_ => None,
}
}
fn default_value(&self, node: NodeId) -> Option<Value> {
let n = &self.nodes[node.0 as usize];
if n.kind == Kind::IndexMark {
return Some(Value::Str(n.onym.clone().unwrap_or_default()));
}
if n.deixis {
return Some(Value::Str(match n.note_edge {
Some(body) => self.nodes[body.0 as usize].prose.clone(),
None => n.onym.clone().unwrap_or_default(),
}));
}
if n.kind == Kind::Ref {
if !n.text.is_empty() {
return Some(Value::Str(n.text.clone()));
}
return Some(Value::Str(match n.ref_edge {
Some(t) => {
let b = &self.nodes[t.0 as usize];
b.lemma.clone().unwrap_or_else(|| b.prose.clone())
}
None => n.target.clone().unwrap_or_default(),
}));
}
if n.kind == Kind::Bib && n.prose.is_empty() && !n.fields.is_empty() {
return Some(Value::Str(
n.fields
.iter()
.map(|(_, v)| v.as_str())
.collect::<Vec<_>>()
.join(". "),
));
}
if n.kind == Kind::Cit {
return Some(Value::Str(n.target.clone().unwrap_or_default()));
}
if n.kind == Kind::Anchor {
return Some(Value::Str(n.onym.clone().unwrap_or_default()));
}
Some(Value::Str(n.prose.clone()))
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
let n = &self.nodes[node.0 as usize];
if let Some(t) = n.ref_edge {
let label = if n.kind == Kind::Cit { "cit" } else { "ref" };
return vec![(label.to_string(), t)];
}
match n.note_edge {
Some(body) => vec![(n.kind.name().unwrap_or("footnote").to_string(), body)],
None => Vec::new(),
}
}
fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
let n = &self.nodes[node.0 as usize];
let mut out: Vec<(String, NodeId)> = n
.ref_cites
.iter()
.map(|&c| {
let label = if self.nodes[c.0 as usize].kind == Kind::Cit {
"cit"
} else {
"ref"
};
(label.to_string(), c)
})
.collect();
let label = n.kind.name().unwrap_or("footnote");
out.extend(n.cites.iter().map(|&c| (label.to_string(), c)));
out
}
fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
let n = &self.nodes[node.0 as usize];
if !matches!(n.kind, Kind::Ref | Kind::Cit) || property != "target" {
return None;
}
let landed = n.ref_edge?;
let is_point = self.nodes[landed.0 as usize].kind == Kind::Anchor;
match hint {
None | Some("*") => Some(landed),
Some("point") => is_point.then_some(landed),
Some("block") => {
if !is_point {
return Some(landed);
}
let mut at = self.nodes[landed.0 as usize].parent;
while let Some(b) = at {
let bn = &self.nodes[b.0 as usize];
if bn.kind != Kind::Document
&& !matches!(bn.kind, Kind::Ref | Kind::Anchor | Kind::IndexMark)
{
return Some(b);
}
at = bn.parent;
}
None
}
Some(_) => None,
}
}
fn ref_property(&self, node: NodeId) -> Option<String> {
matches!(self.nodes[node.0 as usize].kind, Kind::Ref | Kind::Cit)
.then(|| "target".to_string())
}
fn external_ref(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<String> {
if !matches!(hint, None | Some("*")) {
return None;
}
let n = &self.nodes[node.0 as usize];
if n.kind != Kind::Ref || property != "target" || n.internal {
return None;
}
let t = n.target.as_deref()?.trim();
if t.is_empty() || t.starts_with('#') {
return None;
}
let u = match url::Url::parse(t) {
Ok(u) => u,
Err(url::ParseError::RelativeUrlWithoutBase) => {
self.document_url.as_ref()?.join(t).ok()?
}
Err(_) => return None,
};
if u.scheme() != "http" && u.scheme() != "https" {
return None;
}
Some(u.into())
}
fn resolve_fragment(&self, _node: NodeId, fragment: &str) -> Option<NodeId> {
self.onyms.get(fragment).copied()
}
fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
&["level", "lang", "form"]
}
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),
"resolved" if n.deixis => Some(Value::Bool(!n.dangling)),
"resolved" if n.kind == Kind::Cit || (n.kind == Kind::Ref && n.internal) => {
Some(Value::Bool(n.ref_edge.is_some()))
}
"form" if n.margin => Some(Value::Str("margin".to_string())),
"genus" if n.kind == Kind::Bib => n.genus.clone().map(Value::Str),
_ => None,
}
}
}