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 },
Dialogue {
lemma: String,
text: String,
lit: bool,
},
Speech {
text: String,
genoses: Vec<String>,
fields: Vec<(String, String)>,
},
Span {
kind: String,
lo: u32,
hi: u32,
genoses: Vec<String>,
fields: Vec<(String, String)>,
},
Milestone {
scheme: String,
value: String,
at: u32,
},
Annotate { fields: Vec<(String, String)> },
Parsed {
lo: u32,
hi: u32,
onym: Option<String>,
parsings: Vec<Parsing>,
},
Periodos { lo: u32, hi: u32, id: String },
Character {
lemma: String,
onym: Option<String>,
text: String,
genoses: Vec<String>,
},
Wear { genoses: Vec<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,
at: Option<u32>,
},
IndexMark { term: String },
Ref {
target: String,
text: Option<String>,
internal: bool,
},
Anchor { onym: String },
Cite {
target: String,
locator: Option<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, Default, PartialEq, Eq)]
pub struct Parsing {
pub lexema: Option<String>,
pub meros: Option<String>,
pub parepomena: Option<String>,
pub semasia: Option<String>,
pub kephale: Option<String>,
pub schesis: Option<String>,
}
#[derive(Debug, Clone)]
struct DeclaredToken {
lo: u32,
hi: u32,
onym: Option<String>,
parsings: Vec<Parsing>,
}
#[derive(Debug, Clone)]
pub struct CastRow {
pub id: String,
pub name: String,
pub pattern: Option<regex::Regex>,
pub words: usize,
pub fields: Vec<(String, String)>,
}
impl CastRow {
pub fn read_csv(path: &str) -> Result<Vec<CastRow>, String> {
let mut reader = csv::Reader::from_path(path).map_err(|e| e.to_string())?;
let headers: Vec<String> = reader
.headers()
.map_err(|e| e.to_string())?
.iter()
.map(str::to_string)
.collect();
if !headers.iter().any(|h| h == "id") {
return Err(format!(
"a cast table needs an id column (found: {})",
headers.join(", ")
));
}
let mut rows = Vec::new();
for record in reader.records() {
let record = record.map_err(|e| e.to_string())?;
let get = |k: &str| {
headers
.iter()
.position(|h| h == k)
.and_then(|i| record.get(i))
.map(str::trim)
.filter(|v| !v.is_empty())
};
let Some(id) = get("id") else { continue };
let (pattern, words) = if let Some(p) = get("pattern") {
let re = regex::Regex::new(p).map_err(|e| format!("cast pattern for {id}: {e}"))?;
(Some(re), p.split_whitespace().count().max(1))
} else if let Some(stem) = get("stem").or_else(|| get("prefix")) {
let re = regex::Regex::new(&format!("^{}", regex::escape(stem))).unwrap();
(Some(re), stem.split_whitespace().count())
} else if let Some(form) = get("mention").or_else(|| get("form")) {
let re = regex::Regex::new(&format!("^{}$", regex::escape(form))).unwrap();
(Some(re), form.split_whitespace().count())
} else {
(None, 0)
};
let fields: Vec<(String, String)> = headers
.iter()
.zip(record.iter())
.filter(|(h, v)| h.as_str() != "id" && !v.trim().is_empty())
.map(|(h, v)| (h.clone(), v.trim().to_string()))
.collect();
rows.push(CastRow {
id: id.to_string(),
name: get("name").unwrap_or(id).to_string(),
pattern,
words,
fields,
});
}
Ok(rows)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Kind {
Document,
Section,
Character,
Place,
Paragraph,
Blockquote,
UnorderedList,
OrderedList,
UnorderedItem,
OrderedItem,
Verbatim,
Footnote,
Endnote,
Aside,
IndexMark,
Ref,
Cit,
Bib,
Anchor,
Verse,
Strophe,
Stichos,
Token,
Sentence,
Speech,
Dialogue,
Span,
Milestone,
}
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",
Kind::Token => "token",
Kind::Sentence => "sentence",
Kind::Speech => "dialogue-line",
Kind::Character => "character",
Kind::Place => "place",
Kind::Dialogue => "dialogue",
Kind::Span => "span",
Kind::Milestone => "milestone",
})
}
}
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>,
locator: 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>,
token: Option<TokenInfo>,
derived: bool,
genoses: Vec<String>,
span_kind: Option<String>,
at: Option<(u32, u32)>,
span_tokens: Vec<NodeId>,
character: Option<NodeId>,
mentions: Vec<NodeId>,
place: Option<NodeId>,
declared_tokens: Vec<DeclaredToken>,
declared_sentences: Vec<(u32, u32, String)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TokenClass {
Word,
Number,
Punct,
}
impl TokenClass {
fn name(self) -> &'static str {
match self {
TokenClass::Word => "word",
TokenClass::Number => "number",
TokenClass::Punct => "punct",
}
}
}
#[derive(Debug, Clone)]
struct TokenInfo {
class: TokenClass,
n: u32,
sentence: u32,
span: (u32, u32),
annot: Option<Box<Annot>>,
at: u32,
spans: Vec<NodeId>,
}
#[derive(Debug, Clone, Default)]
struct Annot {
id: String,
lemma: Option<String>,
upos: Option<String>,
xpos: Option<String>,
feats: Option<String>,
deprel: Option<String>,
deps: Option<String>,
misc: Option<String>,
keys: Vec<(String, String)>,
mwt: Option<(String, String)>,
empty: bool,
head: Option<NodeId>,
dependents: Vec<NodeId>,
eheads: Vec<(NodeId, String)>,
edependents: Vec<(NodeId, String)>,
mentions: Vec<u32>,
}
#[derive(Debug, Clone)]
struct Mention {
tokens: Vec<NodeId>,
etype: Option<String>,
cluster: Option<String>,
}
impl Annot {
fn key(&self, name: &str) -> Option<&str> {
self.keys
.iter()
.find(|(k, _)| k == name)
.or_else(|| self.keys.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)))
.map(|(_, v)| v.as_str())
}
}
fn entity_entries(spec: &str) -> Vec<String> {
let mut out = Vec::new();
for piece in spec.split('|') {
let mut cur = String::new();
for ch in piece.chars() {
if ch == '(' && !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
cur.push(ch);
if ch == ')' {
out.push(std::mem::take(&mut cur));
}
}
if !cur.is_empty() {
out.push(cur);
}
}
out
}
fn key_values(column: &str) -> Vec<(String, String)> {
if column == "_" || column.is_empty() {
return Vec::new();
}
column
.split('|')
.filter(|e| !e.is_empty())
.map(|e| match e.split_once('=') {
Some((k, v)) => (k.to_string(), v.to_string()),
None => (e.to_string(), String::new()),
})
.collect()
}
fn sentence_spans(prose: &str, bonds: Option<&syndesmos::Syndesmos>) -> Vec<(usize, usize)> {
let raw: Vec<(usize, &str)> = match bonds {
Some(b) => b.split_sentence_bound_indices(prose),
None => {
use unicode_segmentation::UnicodeSegmentation;
prose.split_sentence_bound_indices().collect()
}
};
raw.into_iter()
.filter(|(_, seg)| !seg.trim().is_empty())
.map(|(at, seg)| (at, at + seg.len()))
.collect()
}
const CONLLU_COLUMNS: [&str; 10] = [
"ID", "FORM", "LEMMA", "UPOS", "XPOS", "FEATS", "HEAD", "DEPREL", "DEPS", "MISC",
];
#[derive(Debug, Clone, Default)]
struct ConlluToken {
id: String,
range: Option<(u32, u32)>,
ord: u32,
empty: bool,
form: String,
lemma: String,
upos: String,
xpos: String,
feats: String,
head: String,
deprel: String,
deps: String,
misc: String,
extra: Vec<(String, String)>,
}
#[derive(Debug, Clone, Default)]
struct ConlluSentence {
comments: Vec<(String, String)>,
newdoc: Option<Option<String>>,
newpar: Option<Option<String>>,
tokens: Vec<ConlluToken>,
}
#[derive(Debug, Clone, Default)]
struct ConlluDoc {
sentences: Vec<ConlluSentence>,
globals: Vec<(String, String)>,
}
pub fn looks_like_conllu(text: &str) -> bool {
let mut columns = CONLLU_COLUMNS.len();
for line in text.lines().take(200) {
let line = line.trim_end_matches('\r');
if line.trim().is_empty() {
continue;
}
if let Some(c) = line.strip_prefix('#') {
if let Some((k, v)) = c.split_once('=')
&& k.trim() == "global.columns"
{
columns = v.split_whitespace().count().max(2);
}
continue;
}
let fields: Vec<&str> = line.split('\t').collect();
if fields.len() != columns {
return false;
}
let id = fields[0];
let (a, b) = match id.split_once(['-', '.']) {
Some((a, b)) => (a, Some(b)),
None => (id, None),
};
return a.parse::<u32>().is_ok() && b.is_none_or(|b| b.parse::<u32>().is_ok());
}
false
}
pub fn read_conllu_dir(dir: &std::path::Path) -> Result<Vec<(String, String)>, String> {
fn walk(
dir: &std::path::Path,
base: &std::path::Path,
out: &mut Vec<(String, String)>,
) -> Result<(), String> {
let mut entries: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
.map_err(|e| format!("reading {}: {e}", dir.display()))?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| {
!p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with('.'))
})
.collect();
entries.sort();
for p in entries {
if p.is_dir() {
walk(&p, base, out)?;
} else if p.extension().and_then(|e| e.to_str()).is_some_and(|e| {
e.eq_ignore_ascii_case("conllu") || e.eq_ignore_ascii_case("conllup")
}) {
let text = std::fs::read_to_string(&p)
.map_err(|e| format!("reading {}: {e}", p.display()))?;
let text = text
.strip_prefix('\u{feff}')
.map(str::to_owned)
.unwrap_or(text);
let name = p
.strip_prefix(base)
.unwrap_or(&p)
.to_string_lossy()
.into_owned();
out.push((name, text));
}
}
Ok(())
}
let mut out = Vec::new();
walk(dir, dir, &mut out)?;
if out.is_empty() {
return Err(format!("{} holds no .conllu file", dir.display()));
}
Ok(out)
}
fn parse_conllu(text: &str) -> Result<ConlluDoc, String> {
let mut doc = ConlluDoc::default();
let mut current = ConlluSentence::default();
let mut columns: Vec<String> = CONLLU_COLUMNS.iter().map(|c| c.to_string()).collect();
let trimmed = |s: &str| s.trim().to_string();
for (i, line) in text.lines().enumerate() {
let line = line.trim_end_matches('\r');
if line.trim().is_empty() {
if !current.tokens.is_empty() {
doc.sentences.push(std::mem::take(&mut current));
}
continue;
}
if let Some(comment) = line.strip_prefix('#') {
let comment = comment.trim();
if let Some(rest) = comment.strip_prefix("newdoc") {
let id = rest
.trim()
.strip_prefix("id")
.and_then(|r| r.trim().strip_prefix('='));
current.newdoc = Some(id.map(trimmed));
} else if let Some(rest) = comment.strip_prefix("newpar") {
let id = rest
.trim()
.strip_prefix("id")
.and_then(|r| r.trim().strip_prefix('='));
current.newpar = Some(id.map(trimmed));
} else if let Some((k, v)) = comment.split_once('=') {
let (k, v) = (k.trim(), v.trim());
if let Some(g) = k.strip_prefix("global.") {
if g == "columns" {
columns = v.split_whitespace().map(|c| c.to_string()).collect();
}
doc.globals.push((g.to_string(), v.to_string()));
} else {
current.comments.push((k.to_string(), v.to_string()));
}
}
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 2 {
return Err(format!(
"CoNLL-U line {}: expected tab-separated columns",
i + 1
));
}
let mut tok = ConlluToken::default();
for (k, name) in columns.iter().enumerate() {
let v = f.get(k).copied().unwrap_or("_");
match name.to_ascii_uppercase().as_str() {
"ID" => tok.id = v.to_string(),
"FORM" => tok.form = v.to_string(),
"LEMMA" => tok.lemma = v.to_string(),
"UPOS" => tok.upos = v.to_string(),
"XPOS" => tok.xpos = v.to_string(),
"FEATS" => tok.feats = v.to_string(),
"HEAD" => tok.head = v.to_string(),
"DEPREL" => tok.deprel = v.to_string(),
"DEPS" => tok.deps = v.to_string(),
"MISC" => tok.misc = v.to_string(),
_ => {
if v != "_" && !v.is_empty() {
tok.extra.push((name.replace(':', "-"), v.to_string()));
}
}
}
}
if tok.id.is_empty() {
return Err(format!("CoNLL-U line {}: no token id", i + 1));
}
if let Some((a, b)) = tok.id.split_once('-') {
match (a.parse::<u32>(), b.parse::<u32>()) {
(Ok(a), Ok(b)) if a <= b => {
tok.range = Some((a, b));
tok.ord = a;
}
_ => {
return Err(format!(
"CoNLL-U line {}: malformed multiword range {:?}",
i + 1,
tok.id
));
}
}
} else if let Some((a, _)) = tok.id.split_once('.') {
tok.empty = true;
tok.ord = a.parse().map_err(|_| {
format!(
"CoNLL-U line {}: malformed empty-node id {:?}",
i + 1,
tok.id
)
})?;
} else {
tok.ord = tok
.id
.parse()
.map_err(|_| format!("CoNLL-U line {}: malformed token id {:?}", i + 1, tok.id))?;
}
current.tokens.push(tok);
}
if !current.tokens.is_empty() {
doc.sentences.push(current);
}
Ok(doc)
}
fn conllu_surface(tokens: &[ConlluToken]) -> String {
let mut out = String::new();
let mut covered: u32 = 0; for t in tokens {
if t.empty || (t.range.is_none() && t.ord <= covered) {
continue;
}
if let Some((_, b)) = t.range {
covered = b;
}
out.push_str(&t.form);
let no_space = key_values(&t.misc)
.iter()
.any(|(k, v)| k.eq_ignore_ascii_case("SpaceAfter") && v == "No");
if !no_space {
out.push(' ');
}
}
out.trim_end().to_string()
}
fn class_of(seg: &str) -> TokenClass {
if seg.chars().any(char::is_alphabetic) {
TokenClass::Word
} else if seg.chars().any(char::is_numeric) {
TokenClass::Number
} else {
TokenClass::Punct
}
}
fn class_of_annotated(form: &str, upos: &str) -> TokenClass {
match upos {
"PUNCT" => TokenClass::Punct,
"NUM" => TokenClass::Number,
_ => class_of(form),
}
}
fn is_quote(form: &str) -> bool {
!form.is_empty()
&& form.chars().all(|c| {
matches!(
c,
'"' | '\'' | '`'
| '\u{AB}' | '\u{BB}'
| '\u{2018}'..='\u{201F}'
| '\u{2039}' | '\u{203A}'
| '\u{300C}'..='\u{300F}'
| '\u{301D}'..='\u{301F}'
| '\u{FF02}' | '\u{FF07}'
)
})
}
impl Node {
fn new(kind: Kind, parent: Option<NodeId>) -> Self {
Node {
kind,
token: None,
derived: false,
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,
locator: None,
internal: false,
fields: Vec::new(),
genus: None,
ref_edge: None,
ref_cites: Vec::new(),
parent,
children: Vec::new(),
genoses: Vec::new(),
span_kind: None,
at: None,
span_tokens: Vec::new(),
character: None,
place: None,
mentions: Vec::new(),
declared_tokens: Vec::new(),
declared_sentences: 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(" ")
}
#[derive(Debug, Clone, Default)]
pub struct HeadMeta {
pub declared: Vec<(String, Vec<String>)>,
pub title: Option<String>,
pub description: Option<String>,
pub author: Option<String>,
pub category: Option<String>,
pub tags: Vec<String>,
pub published: Option<Value>,
pub modified: Option<Value>,
}
impl HeadMeta {
pub fn declare(&mut self, name: &str, value: &str) {
let value = normalize_ws(value);
if name.is_empty() || value.is_empty() {
return;
}
match self.declared.iter_mut().find(|(k, _)| k == name) {
Some((_, vs)) => vs.push(value),
None => self.declared.push((name.to_string(), vec![value])),
}
}
pub fn tag(&mut self, tag: &str) {
let tag = normalize_ws(tag);
if tag.is_empty() || self.tags.iter().any(|t| t.eq_ignore_ascii_case(&tag)) {
return;
}
self.tags.push(tag);
}
pub fn declared_value(&self, name: &str) -> Option<Value> {
let (_, vs) = self.declared.iter().find(|(k, _)| k == name)?;
Some(match vs.as_slice() {
[one] => Value::Str(one.clone()),
many => Value::list(many.iter().cloned().map(Value::Str).collect()),
})
}
pub fn trait_name(s: &str) -> String {
let mut out = String::new();
let mut dash = false;
for c in s.chars() {
if c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '+') {
out.push(c);
dash = c == '-';
} else if !dash && !out.is_empty() {
out.push('-');
dash = true;
}
}
while out.ends_with('-') {
out.pop();
}
out
}
}
pub struct TextModel {
nodes: Vec<Node>,
root: NodeId,
onyms: std::collections::HashMap<String, NodeId>,
document_url: Option<url::Url>,
link_base: Option<url::Url>,
document_path: Option<String>,
cite_scheme: Option<String>,
orthography: Option<String>,
bib_aliases: std::collections::HashMap<String, String>,
head: HeadMeta,
token_index: std::sync::OnceLock<std::collections::HashMap<String, Vec<NodeId>>>,
mentions: Vec<Mention>,
}
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::Dialogue { lemma, text, lit } => {
let text = normalize_ws(&text);
if text.is_empty() {
continue;
}
let parent = cursor(§ions, &containers, root);
let kind = if lit { Kind::Dialogue } else { Kind::Paragraph };
let id = push(&mut nodes, kind, parent);
let n = &mut nodes[id.0 as usize];
n.text = text;
n.lemma = Some(normalize_ws(&lemma)).filter(|l| !l.is_empty());
last_flow = Some(id);
last_block = Some(id);
}
Block::Speech {
text,
genoses,
fields,
} => {
let text = normalize_ws(&text);
if text.is_empty() {
continue;
}
let parent = cursor(§ions, &containers, root);
let id = push(&mut nodes, Kind::Speech, parent);
let n = &mut nodes[id.0 as usize];
n.text = text;
n.genoses = genoses;
n.fields = fields;
last_flow = Some(id);
last_block = Some(id);
}
Block::Span {
kind,
lo,
hi,
genoses,
fields,
} => {
let Some(flow) = last_flow else { continue };
let text = nodes[flow.0 as usize]
.text
.get(lo as usize..hi as usize)
.unwrap_or("")
.to_string();
let parent = enclosing_span(&nodes, flow, lo, hi).unwrap_or(flow);
let id = push(&mut nodes, Kind::Span, parent);
let n = &mut nodes[id.0 as usize];
n.span_kind = Some(kind);
n.at = Some((lo, hi));
n.text = text;
n.genoses = genoses;
n.fields = fields;
}
Block::Character {
lemma,
onym,
text,
genoses,
} => {
let parent = cursor(§ions, &containers, root);
let id = push(&mut nodes, Kind::Character, parent);
let n = &mut nodes[id.0 as usize];
n.lemma = Some(normalize_ws(&lemma)).filter(|l| !l.is_empty());
n.onym = onym;
n.text = normalize_ws(&text);
n.genoses = genoses;
last_block = Some(id);
}
Block::Parsed {
lo,
hi,
onym,
parsings,
} => {
let Some(flow) = last_flow else { continue };
nodes[flow.0 as usize].declared_tokens.push(DeclaredToken {
lo,
hi,
onym,
parsings,
});
}
Block::Periodos { lo, hi, id } => {
let Some(flow) = last_flow else { continue };
nodes[flow.0 as usize].declared_sentences.push((lo, hi, id));
}
Block::Milestone { scheme, value, at } => {
let Some(flow) = last_flow else { continue };
let at = if at == u32::MAX {
nodes[flow.0 as usize].text.len() as u32
} else {
at
};
let parent = enclosing_span(&nodes, flow, at, at).unwrap_or(flow);
let id = push(&mut nodes, Kind::Milestone, parent);
let n = &mut nodes[id.0 as usize];
n.at = Some((at, at));
n.fields = vec![("scheme".to_string(), scheme), ("value".to_string(), value)];
}
Block::Annotate { fields } => {
if let Some(b) = last_block {
nodes[b.0 as usize].fields.extend(fields);
}
}
Block::Wear { genoses } => {
if let Some(b) = last_block {
let own = &mut nodes[b.0 as usize].genoses;
for g in genoses {
if !own.contains(&g) {
own.push(g);
}
}
}
}
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,
at,
} => {
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.at = at.map(|a| (a, a));
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, locator } => {
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());
n.locator = locator
.map(|l| l.trim().to_string())
.filter(|l| !l.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);
let mut model = TextModel {
nodes,
root,
onyms,
document_url: None,
link_base: None,
document_path: None,
cite_scheme: None,
orthography: None,
bib_aliases: Default::default(),
head: HeadMeta::default(),
token_index: Default::default(),
mentions: Vec::new(),
};
model.link_characters();
model
}
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 set_link_base(&mut self, url: &str) {
self.link_base = url::Url::parse(url).ok();
}
pub fn set_document_path(&mut self, path: &str) {
self.document_path = Some(path.to_string());
}
pub fn set_cite_scheme(&mut self, scheme: &str) {
self.cite_scheme = Some(scheme.to_string());
}
pub fn set_orthography(&mut self, table: &str) -> Result<(), String> {
quarb::translit::modernize("", table)?;
self.orthography = Some(table.to_string());
Ok(())
}
fn modern(&self, form: &str) -> String {
match &self.orthography {
Some(t) => quarb::translit::modernize(form, t).unwrap_or_else(|_| form.to_string()),
None => form.to_string(),
}
}
fn lower(&self, form: &str) -> String {
self.modern(form).to_lowercase()
}
fn cite_table(&self) -> Vec<(NodeId, u32, NodeId)> {
let mut scheme = self.cite_scheme.clone();
let mut out = Vec::new();
for i in 0..self.nodes.len() {
let n = &self.nodes[i];
if n.kind != Kind::Milestone {
continue;
}
let this = n
.fields
.iter()
.find(|(k, _)| k == "scheme")
.map(|(_, v)| v.clone());
let Some(this) = this else { continue };
match &scheme {
None => scheme = Some(this),
Some(s) if *s != this => continue,
_ => {}
}
let at = n.at.map(|(a, _)| a).unwrap_or(0);
let mut block = NodeId(i as u64);
while self.nodes[block.0 as usize].kind == Kind::Span
|| self.nodes[block.0 as usize].kind == Kind::Milestone
{
match self.nodes[block.0 as usize].parent {
Some(p) => block = p,
None => break,
}
}
out.push((block, at, NodeId(i as u64)));
}
out.sort_by_key(|(b, at, _)| (b.0, *at));
out
}
fn cite(&self, node: NodeId) -> Option<String> {
let table = self.cite_table();
if table.is_empty() {
return self.structural_cite(node);
}
let n = &self.nodes[node.0 as usize];
let value = |m: NodeId| {
self.nodes[m.0 as usize]
.fields
.iter()
.find(|(k, _)| k == "value")
.map(|(_, v)| v.clone())
};
let container = matches!(n.kind, Kind::Document | Kind::Section)
|| (n.kind != Kind::Span
&& n.kind != Kind::Milestone
&& n.token.is_none()
&& n.kind != Kind::Sentence
&& !self.prose_blocks().contains(&node)
&& !n.children.is_empty()
&& n.prose.is_empty());
if container {
let inside = |mut b: NodeId| {
loop {
if b == node {
return true;
}
match self.nodes[b.0 as usize].parent {
Some(p) => b = p,
None => return false,
}
}
};
return table
.iter()
.find(|(b, _, _)| inside(*b))
.and_then(|(_, _, m)| value(*m));
}
let body = if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis {
Some(node)
} else {
self.note_body_of(node)
};
if let Some(body) = body {
let callout = (0..self.nodes.len())
.find(|&i| self.nodes[i].deixis && self.nodes[i].note_edge == Some(body))
.map(|i| NodeId(i as u64));
return callout.and_then(|c| self.cite(c));
}
let (block, at) = match n.kind {
_ if n.deixis && n.at.is_some() => {
(n.parent.unwrap_or(node), n.at.map(|(a, _)| a).unwrap_or(0))
}
Kind::Milestone | Kind::Span => {
let mut b = node;
while matches!(self.nodes[b.0 as usize].kind, Kind::Span | Kind::Milestone) {
match self.nodes[b.0 as usize].parent {
Some(p) => b = p,
None => break,
}
}
(b, n.at.map(|(a, _)| a).unwrap_or(0))
}
_ if n.token.is_some() => {
let mut b = node;
while self.nodes[b.0 as usize].token.is_some()
|| self.nodes[b.0 as usize].kind == Kind::Sentence
{
match self.nodes[b.0 as usize].parent {
Some(p) => b = p,
None => break,
}
}
(b, n.token.as_ref().map(|t| t.at).unwrap_or(0))
}
Kind::Sentence if n.derived => {
let mut toks = Vec::new();
self.collect_tokens(node, &mut toks);
let at = toks
.first()
.and_then(|t| self.nodes[t.0 as usize].token.as_ref().map(|t| t.at))
.unwrap_or(0);
(n.parent.unwrap_or(node), at)
}
_ => (node, 0),
};
table
.iter()
.rev()
.find(|(b, a, _)| (b.0, *a) <= (block.0, at))
.and_then(|(_, _, m)| value(*m))
.or_else(|| self.structural_cite(node))
}
fn structural_cite(&self, node: NodeId) -> Option<String> {
let mut names: Vec<String> = Vec::new();
let mut cur = Some(node);
while let Some(n) = cur {
let node = &self.nodes[n.0 as usize];
if node.kind == Kind::Section
&& let Some(l) = &node.lemma
&& !l.is_empty()
{
names.push(l.clone());
}
cur = node.parent;
}
if names.is_empty() {
return None;
}
names.reverse();
Some(names.join(" / "))
}
fn href_base(&self) -> Option<String> {
if let Some(u) = &self.document_url {
return Some(u.to_string());
}
self.document_path.clone()
}
fn nearest_onym(&self, node: NodeId) -> Option<(String, bool)> {
let mut cur = Some(node);
let mut exact = true;
while let Some(id) = cur {
let n = &self.nodes[id.0 as usize];
if n.kind != Kind::Document
&& !n.deixis
&& let Some(o) = &n.onym
{
return Some((o.clone(), exact));
}
exact = false;
cur = n.parent;
}
None
}
fn href(&self, node: NodeId) -> Option<String> {
let base = self.href_base()?;
let n = &self.nodes[node.0 as usize];
if n.kind == Kind::Document {
return Some(base);
}
let served = base.split(['?', '#']).next().unwrap_or(&base);
let last = served.rsplit('/').next().unwrap_or(served);
let html = served.ends_with('/')
|| !last.contains('.')
|| last.ends_with(".html")
|| last.ends_with(".htm");
if !html {
return Some(base);
}
let mut out = base;
let (anchor, exact) = match self.nearest_onym(node) {
Some((a, exact)) => (Some(a), exact),
None => (None, false),
};
if let Some(a) = &anchor {
out.push('#');
out.push_str(a);
}
if !exact {
let words: Vec<&str> = n.prose.split_whitespace().collect();
if !words.is_empty() {
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
let enc =
|ws: &[&str]| utf8_percent_encode(&ws.join(" "), NON_ALPHANUMERIC).to_string();
if anchor.is_none() {
out.push('#');
}
out.push_str(":~:text=");
if words.len() <= 12 {
out.push_str(&enc(&words));
} else {
out.push_str(&enc(&words[..6]));
out.push(',');
out.push_str(&enc(&words[words.len() - 6..]));
}
}
}
Some(out)
}
fn document_title(&self) -> Option<String> {
if let Some(t) = &self.head.title {
return Some(t.clone());
}
self.nodes[self.root.0 as usize]
.children
.iter()
.map(|c| &self.nodes[c.0 as usize])
.find(|n| n.kind == Kind::Section)
.and_then(|n| n.lemma.clone())
}
pub fn set_head_meta(&mut self, head: HeadMeta) {
self.head = head;
}
pub fn head_meta(&self) -> &HeadMeta {
&self.head
}
pub fn plain_text(&self) -> String {
let mut out = String::new();
let mut stack = vec![self.root];
while let Some(id) = stack.pop() {
let n = &self.nodes[id.0 as usize];
if let Some(l) = &n.lemma
&& !l.is_empty()
{
out.push_str(l);
out.push('\n');
}
if !n.prose.is_empty() && n.kind != Kind::Document {
out.push_str(&n.prose);
out.push('\n');
}
for c in n.children.iter().rev() {
stack.push(*c);
}
}
out
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
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)
}
impl TextModel {
pub fn apply_cast(&mut self, rows: &[CastRow]) {
for row in rows {
let known = self
.nodes
.iter()
.any(|n| n.kind == Kind::Character && n.onym.as_deref() == Some(row.id.as_str()));
if known {
continue;
}
let root = NodeId(0);
let id = push(&mut self.nodes, Kind::Character, root);
let n = &mut self.nodes[id.0 as usize];
n.lemma = Some(row.name.clone()).filter(|l| !l.is_empty());
n.onym = Some(row.id.clone());
n.fields = row.fields.clone();
}
let patterns: Vec<(&CastRow, ®ex::Regex, usize)> = rows
.iter()
.filter_map(|r| r.pattern.as_ref().map(|p| (r, p, r.words.max(1))))
.collect();
let widest = patterns.iter().map(|(_, _, w)| *w).max().unwrap_or(0);
if widest > 0 {
let words: Vec<usize> = (0..self.nodes.len())
.filter(|&i| {
self.nodes[i]
.token
.as_ref()
.is_some_and(|t| t.class == TokenClass::Word)
})
.collect();
for (k, &i) in words.iter().enumerate() {
if self.nodes[i].fields.iter().any(|(k, _)| k == "prosopon") {
continue;
}
let mut run = vec![self.nodes[i].text.as_str()];
for &j in words.iter().skip(k + 1).take(widest - 1) {
if self.nodes[j].parent != self.nodes[i].parent {
break;
}
run.push(self.nodes[j].text.as_str());
}
let hit = patterns.iter().find(|(_, p, w)| {
(1..=(*w).min(run.len()))
.rev()
.any(|n| p.is_match(&run[..n].join(" ")))
});
if let Some((row, _, _)) = hit {
self.nodes[i]
.fields
.push(("prosopon".to_string(), row.id.clone()));
}
}
}
let chains: Vec<(&CastRow, &str)> = rows
.iter()
.filter_map(|r| {
r.fields
.iter()
.find(|(k, _)| k == "cluster" || k == "chain")
.map(|(_, v)| (r, v.as_str()))
})
.collect();
if !chains.is_empty() {
let firsts: Vec<(NodeId, String)> = self
.mentions
.iter()
.filter_map(|m| {
let c = m.cluster.as_deref()?;
let (row, _) = chains.iter().find(|(_, id)| *id == c)?;
Some((*m.tokens.first()?, row.id.clone()))
})
.collect();
for (t, id) in firsts {
let f = &mut self.nodes[t.0 as usize].fields;
if !f.iter().any(|(k, _)| k == "prosopon") {
f.push(("prosopon".to_string(), id));
}
}
}
if !patterns.is_empty() {
for i in 0..self.nodes.len() {
let n = &self.nodes[i];
if n.kind != Kind::Dialogue || n.fields.iter().any(|(k, _)| k == "prosopon") {
continue;
}
let Some(label) = n.lemma.clone() else {
continue;
};
if let Some((row, _, _)) = patterns.iter().find(|(_, p, _)| p.is_match(&label)) {
self.nodes[i]
.fields
.push(("prosopon".to_string(), row.id.clone()));
}
}
}
self.link_characters();
}
fn link_characters(&mut self) {
let mut by_onym: std::collections::HashMap<String, NodeId> =
std::collections::HashMap::new();
for (i, n) in self.nodes.iter().enumerate() {
if n.kind == Kind::Character
&& let Some(o) = &n.onym
{
by_onym.entry(o.clone()).or_insert(NodeId(i as u64));
}
}
for n in &mut self.nodes {
n.mentions.clear();
}
for i in 0..self.nodes.len() {
if self.nodes[i].kind == Kind::Character {
continue;
}
let target = self.nodes[i]
.fields
.iter()
.find(|(k, _)| k == "prosopon")
.and_then(|(_, v)| by_onym.get(v).copied());
self.nodes[i].character = target;
if let Some(c) = target {
self.nodes[c.0 as usize].mentions.push(NodeId(i as u64));
}
}
}
pub fn apply_places(&mut self, rows: &[CastRow]) {
for row in rows {
let known = self
.nodes
.iter()
.any(|n| n.kind == Kind::Place && n.onym.as_deref() == Some(row.id.as_str()));
if known {
continue;
}
let root = NodeId(0);
let id = push(&mut self.nodes, Kind::Place, root);
let n = &mut self.nodes[id.0 as usize];
n.lemma = Some(row.name.clone()).filter(|l| !l.is_empty());
n.onym = Some(row.id.clone());
n.fields = row.fields.clone();
}
let patterns: Vec<(&CastRow, ®ex::Regex, usize)> = rows
.iter()
.filter_map(|r| r.pattern.as_ref().map(|p| (r, p, r.words.max(1))))
.collect();
let widest = patterns.iter().map(|(_, _, w)| *w).max().unwrap_or(0);
if widest > 0 {
let words: Vec<usize> = (0..self.nodes.len())
.filter(|&i| {
self.nodes[i]
.token
.as_ref()
.is_some_and(|t| t.class == TokenClass::Word)
})
.collect();
for (k, &i) in words.iter().enumerate() {
if self.nodes[i].fields.iter().any(|(k, _)| k == "chora") {
continue;
}
let mut run = vec![self.nodes[i].text.as_str()];
for &j in words.iter().skip(k + 1).take(widest - 1) {
if self.nodes[j].parent != self.nodes[i].parent {
break;
}
run.push(self.nodes[j].text.as_str());
}
let hit = patterns.iter().find(|(_, p, w)| {
(1..=(*w).min(run.len()))
.rev()
.any(|n| p.is_match(&run[..n].join(" ")))
});
if let Some((row, _, _)) = hit {
self.nodes[i]
.fields
.push(("chora".to_string(), row.id.clone()));
}
}
}
self.link_places();
}
fn link_places(&mut self) {
let mut by_onym: std::collections::HashMap<String, NodeId> =
std::collections::HashMap::new();
for (i, n) in self.nodes.iter().enumerate() {
if n.kind == Kind::Place
&& let Some(o) = &n.onym
{
by_onym.entry(o.clone()).or_insert(NodeId(i as u64));
}
}
for n in &mut self.nodes {
if n.kind == Kind::Place {
n.mentions.clear();
}
}
for i in 0..self.nodes.len() {
if self.nodes[i].kind == Kind::Place {
continue;
}
let target = self.nodes[i]
.fields
.iter()
.find(|(k, _)| k == "chora")
.and_then(|(_, v)| by_onym.get(v).copied());
self.nodes[i].place = target;
if let Some(c) = target {
self.nodes[c.0 as usize].mentions.push(NodeId(i as u64));
}
}
}
pub fn tokenize(&mut self) {
self.tokenize_with(None)
}
fn prose_blocks(&self) -> Vec<NodeId> {
(0..self.nodes.len())
.filter(|&id| {
let node = &self.nodes[id];
let prose_block = match node.kind {
Kind::Paragraph => !node.children.iter().any(|c| {
let c = &self.nodes[c.0 as usize];
c.kind == Kind::Sentence && !c.derived
}),
Kind::Sentence => !node.derived,
Kind::UnorderedItem | Kind::OrderedItem | Kind::Stichos => true,
Kind::Speech | Kind::Dialogue => true,
Kind::Footnote | Kind::Endnote | Kind::Aside => !node.deixis,
_ => false,
};
prose_block && !node.prose.is_empty() && !self.in_apparatus(NodeId(id as u64))
})
.map(|id| NodeId(id as u64))
.collect()
}
fn note_body_of(&self, node: NodeId) -> Option<NodeId> {
let mut cur = self.nodes[node.0 as usize].parent;
while let Some(p) = cur {
let n = &self.nodes[p.0 as usize];
if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis {
return Some(p);
}
cur = n.parent;
}
None
}
fn in_apparatus(&self, node: NodeId) -> bool {
let n = &self.nodes[node.0 as usize];
(matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis)
|| self.note_body_of(node).is_some()
}
pub fn corpus_text(&self) -> String {
self.prose_blocks()
.iter()
.map(|&b| self.nodes[b.0 as usize].prose.as_str())
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn is_tokenized(&self) -> bool {
self.nodes.iter().any(|n| n.kind == Kind::Token)
}
pub fn tokenize_with(&mut self, bonds: Option<&syndesmos::Syndesmos>) {
use unicode_segmentation::UnicodeSegmentation;
self.token_index = Default::default();
let mut n: u32 = 0;
let mut sentence: u32 = 0;
for block in self.prose_blocks() {
let id = block.0 as usize;
let prose = self.nodes[id].prose.clone();
let declared_sentences = std::mem::take(&mut self.nodes[id].declared_sentences);
let mut declared_tokens = std::mem::take(&mut self.nodes[id].declared_tokens);
declared_tokens.sort_by_key(|t| t.lo);
let mut spans = if declared_sentences.is_empty() {
sentence_spans(&prose, bonds)
} else {
declared_sentences
.iter()
.map(|(lo, hi, _)| (*lo as usize, *hi as usize))
.filter(|(lo, hi)| lo < hi && *hi <= prose.len())
.collect()
};
if spans.is_empty() {
spans.push((0, prose.len()));
}
let sentence_ids: Vec<Option<String>> = if declared_sentences.is_empty() {
Vec::new()
} else {
declared_sentences
.iter()
.map(|(_, _, sid)| Some(sid.clone()))
.collect()
};
let sentences: Vec<(usize, usize, u32, NodeId)> = spans
.into_iter()
.enumerate()
.map(|(k, (lo, hi))| {
sentence += 1;
let node = self.push_derived_sentence(block, &prose[lo..hi], sentence);
if let Some(Some(sid)) = sentence_ids.get(k) {
self.nodes[node.0 as usize]
.fields
.push(("sent_id".to_string(), sid.clone()));
}
(lo, hi, sentence, node)
})
.collect();
let mut by_onym: Vec<(String, NodeId)> = Vec::new();
let mut heads: Vec<(NodeId, String)> = Vec::new();
let mut next_declared = 0usize;
let mut skip_until = 0usize;
for (at, seg) in prose.split_word_bound_indices() {
if at < skip_until {
continue;
}
while next_declared < declared_tokens.len()
&& (declared_tokens[next_declared].lo as usize) < at
{
next_declared += 1;
}
if let Some(d) = declared_tokens.get(next_declared)
&& d.lo as usize == at
&& (d.hi as usize) > at
&& (d.hi as usize) <= prose.len()
{
let d = d.clone();
next_declared += 1;
skip_until = d.hi as usize;
let form = &prose[d.lo as usize..d.hi as usize];
n += 1;
let (in_sentence, span, parent) = sentences
.iter()
.find(|(lo, hi, _, _)| *lo <= at && at < *hi)
.or_else(|| sentences.last())
.map(|(lo, hi, s, node)| (*s, (*lo as u32, *hi as u32), *node))
.expect("a block has at least one sentence");
let tok = self.push_parsed(parent, form, &d, n, in_sentence);
if let Some(t) = self.nodes[tok.0 as usize].token.as_mut() {
t.span = span;
t.at = at as u32;
}
if let Some(o) = &d.onym {
by_onym.push((o.clone(), tok));
}
if let Some(h) = d.parsings.first().and_then(|p| p.kephale.clone()) {
heads.push((tok, h));
}
continue;
}
if seg.chars().all(char::is_whitespace) {
continue;
}
let class = class_of(seg);
n += 1;
let (in_sentence, span, parent) = sentences
.iter()
.find(|(lo, hi, _, _)| *lo <= at && at < *hi)
.or_else(|| sentences.last())
.map(|(lo, hi, s, node)| (*s, (*lo as u32, *hi as u32), *node))
.expect("a block has at least one sentence");
let tok = push(&mut self.nodes, Kind::Token, parent);
let t = &mut self.nodes[tok.0 as usize];
t.text = seg.to_string();
t.prose = seg.to_string();
t.token = Some(TokenInfo {
class,
n,
sentence: in_sentence,
span,
annot: None,
at: at as u32,
spans: Vec::new(),
});
}
for (tok, h) in heads {
let Some((_, head)) = by_onym.iter().find(|(o, _)| *o == h) else {
continue;
};
let head = *head;
if let Some(a) = self.nodes[tok.0 as usize]
.token
.as_mut()
.and_then(|t| t.annot.as_mut())
{
a.head = Some(head);
}
if let Some(a) = self.nodes[head.0 as usize]
.token
.as_mut()
.and_then(|t| t.annot.as_mut())
{
a.dependents.push(tok);
}
}
}
self.attribute_spans();
}
fn push_parsed(
&mut self,
sentence_node: NodeId,
form: &str,
d: &DeclaredToken,
n: u32,
sentence: u32,
) -> NodeId {
let node = push(&mut self.nodes, Kind::Token, sentence_node);
let t = &mut self.nodes[node.0 as usize];
t.text = form.to_string();
t.prose = form.to_string();
let first = d.parsings.first().cloned().unwrap_or_default();
let (upos, xpos) = match first.meros.as_deref() {
Some(m) => match m.split_once('/') {
Some((u, x)) => (Some(u.to_string()), Some(x.to_string())),
None => (Some(m.to_string()), None),
},
None => (None, None),
};
let feats = first.parepomena.clone();
let mut keys = match feats.as_deref() {
Some(f) if f.split([',', '|']).all(|p| p.contains('=')) => {
key_values(&f.replace(',', "|"))
}
_ => Vec::new(),
};
if let Some(sem) = &first.semasia {
keys.push(("sem".to_string(), sem.clone()));
}
for alt in d.parsings.iter().skip(1) {
let mut v = alt.lexema.clone().unwrap_or_default();
if let Some(m) = &alt.meros {
v.push(' ');
v.push_str(m);
}
if let Some(f) = &alt.parepomena {
v.push(' ');
v.push_str(f);
}
keys.push(("alt".to_string(), v.trim().to_string()));
}
t.token = Some(TokenInfo {
class: class_of_annotated(form, upos.as_deref().unwrap_or("")),
n,
sentence,
span: (0, 0),
at: 0,
spans: Vec::new(),
annot: Some(Box::new(Annot {
id: d.onym.clone().unwrap_or_else(|| n.to_string()),
lemma: first.lexema.clone(),
upos,
xpos,
feats,
deprel: first.schesis.clone(),
deps: None,
misc: None,
keys,
mwt: None,
empty: false,
head: None,
dependents: Vec::new(),
eheads: Vec::new(),
edependents: Vec::new(),
mentions: Vec::new(),
})),
});
node
}
fn attribute_spans(&mut self) {
let spans: Vec<NodeId> = (0..self.nodes.len())
.filter(|&i| self.nodes[i].kind == Kind::Span && self.nodes[i].at.is_some())
.map(|i| NodeId(i as u64))
.collect();
if spans.is_empty() {
return;
}
for span in spans {
let (lo, hi) = self.nodes[span.0 as usize].at.expect("filtered");
let mut block = span;
while self.nodes[block.0 as usize].kind == Kind::Span
&& let Some(p) = self.nodes[block.0 as usize].parent
{
block = p;
}
let mut tokens = Vec::new();
self.collect_tokens(block, &mut tokens);
let covered: Vec<NodeId> = tokens
.into_iter()
.filter(|t| {
let at = self.nodes[t.0 as usize].token.as_ref().map(|t| t.at);
at.is_some_and(|at| lo <= at && at < hi)
})
.collect();
for &t in &covered {
if let Some(info) = self.nodes[t.0 as usize].token.as_mut()
&& !info.spans.contains(&span)
{
info.spans.push(span);
}
}
self.nodes[span.0 as usize].span_tokens = covered;
}
for n in self.nodes.iter_mut() {
if let Some(t) = n.token.as_mut() {
t.spans.sort();
}
}
}
fn collect_tokens(&self, node: NodeId, out: &mut Vec<NodeId>) {
for &c in &self.nodes[node.0 as usize].children {
let n = &self.nodes[c.0 as usize];
if n.token.is_some() {
out.push(c);
} else if n.kind == Kind::Sentence {
self.collect_tokens(c, out);
}
}
}
fn push_derived_sentence(&mut self, block: NodeId, text: &str, ordinal: u32) -> NodeId {
let node = push(&mut self.nodes, Kind::Sentence, block);
let s = &mut self.nodes[node.0 as usize];
s.text = text.trim().to_string();
s.prose = s.text.clone();
s.taxis = Some(ordinal as i64);
s.derived = true;
node
}
pub fn annotate_conllu(&mut self, conllu: &str) -> Result<(), String> {
if self.is_tokenized() {
return Err("the document is already tokenized".into());
}
self.token_index = Default::default();
let blocks = self.prose_blocks();
let mut doc = String::new();
let mut ranges: Vec<(usize, usize, NodeId)> = Vec::new();
for (i, &b) in blocks.iter().enumerate() {
if i > 0 {
doc.push_str("\n\n");
}
let lo = doc.len();
doc.push_str(&self.nodes[b.0 as usize].prose);
ranges.push((lo, doc.len(), b));
}
if ranges.is_empty() {
return Err("the document has no prose to annotate".into());
}
let parsed = parse_conllu(conllu)?;
let mut cursor = 0usize;
let mut block_at = 0usize;
let mut n: u32 = 0;
for (si, sent) in parsed.sentences.iter().enumerate() {
let ordinal = si as u32 + 1;
let mut made: Vec<(NodeId, &ConlluToken)> = Vec::new();
let mut extents: Vec<(NodeId, usize, usize)> = Vec::new();
let mut nodes_by_block: Vec<(NodeId, NodeId)> = Vec::new();
let mut range: Option<(usize, usize, u32, String, String)> = None;
for tok in &sent.tokens {
if !tok.empty && tok.form.trim().is_empty() {
continue;
}
let (lo, hi, mwt) = if tok.empty {
(cursor, cursor, None)
} else if let Some((lo, hi, until, id, form)) = &range
&& tok.range.is_none()
&& tok.ord <= *until
{
(*lo, *hi, Some((id.clone(), form.clone())))
} else {
range = None;
while doc[cursor..].starts_with(char::is_whitespace) {
cursor += doc[cursor..].chars().next().map_or(0, char::len_utf8);
}
if !doc[cursor..].starts_with(tok.form.as_str()) {
let seen: String = doc[cursor..].chars().take(24).collect();
return Err(format!(
"CoNLL-U sentence {ordinal}, token {}: {:?} does not align with the text at offset {cursor} ({seen:?})",
tok.id, tok.form
));
}
let lo = cursor;
cursor += tok.form.len();
if let Some((_, b)) = tok.range {
range = Some((lo, cursor, b, tok.id.clone(), tok.form.clone()));
continue;
}
(lo, cursor, None)
};
if !tok.empty {
while block_at + 1 < ranges.len() && lo >= ranges[block_at].1 {
block_at += 1;
}
}
let (blo, _, block) = ranges[block_at];
if !tok.empty {
n += 1;
}
let sentence_node = match nodes_by_block.iter().find(|(b, _)| *b == block) {
Some((_, s)) => *s,
None => {
let s = self.push_derived_sentence(block, "", ordinal);
nodes_by_block.push((block, s));
s
}
};
let node = self.push_annotated(
sentence_node,
tok,
if tok.empty { 0 } else { n },
ordinal,
mwt,
);
if !tok.empty {
match extents.iter_mut().find(|(b, _, _)| *b == block) {
Some(e) => {
e.1 = e.1.min(lo - blo);
e.2 = e.2.max(hi - blo);
}
None => extents.push((block, lo - blo, hi - blo)),
}
if let Some(t) = self.nodes[node.0 as usize].token.as_mut() {
t.at = (lo - blo) as u32;
}
}
made.push((node, tok));
}
for (block, s) in &nodes_by_block {
if let Some((_, lo, hi)) = extents.iter().find(|(b, _, _)| b == block) {
let text = self.nodes[block.0 as usize].prose[*lo..*hi]
.trim()
.to_string();
let sn = &mut self.nodes[s.0 as usize];
sn.text = text.clone();
sn.prose = text;
}
}
for (node, _) in &made {
let sentence_node = self.nodes[node.0 as usize]
.parent
.expect("token has a sentence");
let block = self.nodes[sentence_node.0 as usize]
.parent
.expect("sentence has a block");
if let Some((_, lo, hi)) = extents.iter().find(|(b, _, _)| *b == block)
&& let Some(t) = self.nodes[node.0 as usize].token.as_mut()
{
t.span = (*lo as u32, *hi as u32);
}
}
self.link_sentence(&made);
}
let header = parsed
.globals
.iter()
.find(|(k, _)| k == "Entity")
.map(|(_, v)| v.as_str());
self.decode_mentions(header);
Ok(())
}
pub fn parse_conllu_text(text: &str) -> Result<Self, String> {
Self::from_conllu_set(&[(None, text)], false)
}
pub fn parse_conllu_corpus(text: &str) -> Result<Self, String> {
Self::from_conllu_set(&[(None, text)], true)
}
pub fn parse_conllu_text_set(files: &[(&str, &str)]) -> Result<Self, String> {
let named: Vec<(Option<&str>, &str)> = files.iter().map(|(n, t)| (Some(*n), *t)).collect();
Self::from_conllu_set(&named, false)
}
pub fn parse_conllu_corpus_set(files: &[(&str, &str)]) -> Result<Self, String> {
let named: Vec<(Option<&str>, &str)> = files.iter().map(|(n, t)| (Some(*n), *t)).collect();
Self::from_conllu_set(&named, true)
}
fn from_conllu_set(files: &[(Option<&str>, &str)], tokens: bool) -> Result<Self, String> {
let mut parsed: Vec<(Option<&str>, ConlluDoc)> = Vec::with_capacity(files.len());
for (name, text) in files {
let doc = parse_conllu(text).map_err(|e| match name {
Some(n) => format!("{n}: {e}"),
None => e,
})?;
parsed.push((*name, doc));
}
let mut nodes = vec![Node::new(Kind::Document, None)];
let root = NodeId(0);
let mut sentences: Vec<(NodeId, usize, usize)> = Vec::new();
for (fi, (name, doc)) in parsed.iter().enumerate() {
let (base, doc_level) = match name {
Some(n) => {
let sec = push(&mut nodes, Kind::Section, root);
nodes[sec.0 as usize].lemma = Some(n.to_string());
nodes[sec.0 as usize].level = Some(1);
(sec, 2)
}
None => (root, 1),
};
let mut section: Option<NodeId> = None;
let mut para: Option<NodeId> = None;
for (si, s) in doc.sentences.iter().enumerate() {
if let Some(id) = &s.newdoc {
let sec = push(&mut nodes, Kind::Section, base);
nodes[sec.0 as usize].lemma = id.clone();
nodes[sec.0 as usize].level = Some(doc_level);
section = Some(sec);
para = None;
}
if s.newpar.is_some() {
let p = push(&mut nodes, Kind::Paragraph, section.unwrap_or(base));
para = Some(p);
}
let parent = para.or(section).unwrap_or(base);
let sent = push(&mut nodes, Kind::Sentence, parent);
nodes[sent.0 as usize].taxis = Some(sentences.len() as i64 + 1);
let text = s
.comments
.iter()
.find(|(k, _)| k == "text")
.map(|(_, v)| v.clone())
.unwrap_or_else(|| conllu_surface(&s.tokens));
nodes[sent.0 as usize].text = text;
nodes[sent.0 as usize].fields = s.comments.clone();
sentences.push((sent, fi, si));
}
}
flatten_prose(&mut nodes);
let mut model = TextModel {
nodes,
root,
onyms: Default::default(),
document_url: None,
link_base: None,
document_path: None,
cite_scheme: None,
orthography: None,
bib_aliases: Default::default(),
head: HeadMeta::default(),
token_index: Default::default(),
mentions: Vec::new(),
};
if tokens {
let mut n: u32 = 0;
for (i, (sent, fi, si)) in sentences.into_iter().enumerate() {
let s = &parsed[fi].1.sentences[si];
let ordinal = i as u32 + 1;
let len = model.nodes[sent.0 as usize].prose.len() as u32;
let mut made: Vec<(NodeId, &ConlluToken)> = Vec::new();
let mut range: Option<(u32, String, String)> = None;
for tok in &s.tokens {
if let Some((_, b)) = tok.range {
range = Some((b, tok.id.clone(), tok.form.clone()));
continue;
}
let mwt = match &range {
Some((until, id, form)) if !tok.empty && tok.ord <= *until => {
Some((id.clone(), form.clone()))
}
_ => {
range = None;
None
}
};
if !tok.empty {
n += 1;
}
let node = model.push_annotated(
sent,
tok,
if tok.empty { 0 } else { n },
ordinal,
mwt,
);
if let Some(t) = model.nodes[node.0 as usize].token.as_mut() {
t.span = (0, len);
}
made.push((node, tok));
}
model.link_sentence(&made);
}
let header = parsed
.iter()
.flat_map(|(_, d)| d.globals.iter())
.find(|(k, _)| k == "Entity")
.map(|(_, v)| v.as_str());
model.decode_mentions(header);
}
Ok(model)
}
fn push_annotated(
&mut self,
block: NodeId,
tok: &ConlluToken,
n: u32,
sentence: u32,
mwt: Option<(String, String)>,
) -> NodeId {
let node = push(&mut self.nodes, Kind::Token, block);
let t = &mut self.nodes[node.0 as usize];
t.text = tok.form.clone();
t.prose = tok.form.clone();
let opt = |v: &str| (v != "_" && !v.is_empty()).then(|| v.to_string());
let mut keys = key_values(&tok.feats);
keys.extend(key_values(&tok.misc));
keys.extend(tok.extra.iter().cloned());
t.token = Some(TokenInfo {
class: class_of_annotated(&tok.form, &tok.upos),
n,
sentence,
span: (0, 0),
at: 0,
spans: Vec::new(),
annot: Some(Box::new(Annot {
id: tok.id.clone(),
lemma: opt(&tok.lemma),
upos: opt(&tok.upos),
xpos: opt(&tok.xpos),
feats: opt(&tok.feats),
deprel: opt(&tok.deprel),
deps: opt(&tok.deps),
misc: opt(&tok.misc),
keys,
mwt,
empty: tok.empty,
head: None,
dependents: Vec::new(),
eheads: Vec::new(),
edependents: Vec::new(),
mentions: Vec::new(),
})),
});
node
}
fn link_sentence(&mut self, made: &[(NodeId, &ConlluToken)]) {
let find = |id: &str| made.iter().find(|(_, t)| t.id == id).map(|(n, _)| *n);
for (node, tok) in made {
if !tok.empty
&& tok.head != "0"
&& tok.head != "_"
&& !tok.head.is_empty()
&& let Some(h) = find(&tok.head)
{
if let Some(a) = self.nodes[node.0 as usize]
.token
.as_mut()
.and_then(|t| t.annot.as_mut())
{
a.head = Some(h);
}
if let Some(a) = self.nodes[h.0 as usize]
.token
.as_mut()
.and_then(|t| t.annot.as_mut())
{
a.dependents.push(*node);
}
}
if tok.deps == "_" || tok.deps.is_empty() {
continue;
}
for entry in tok.deps.split('|') {
let Some((h, rel)) = entry.split_once(':') else {
continue;
};
if h == "0" {
continue;
}
let Some(target) = find(h) else {
continue;
};
if let Some(a) = self.nodes[node.0 as usize]
.token
.as_mut()
.and_then(|t| t.annot.as_mut())
{
a.eheads.push((target, rel.to_string()));
}
if let Some(a) = self.nodes[target.0 as usize]
.token
.as_mut()
.and_then(|t| t.annot.as_mut())
{
a.edependents.push((*node, rel.to_string()));
}
}
}
}
fn decode_mentions(&mut self, entity_header: Option<&str>) {
self.mentions.clear();
let tokens: Vec<NodeId> = (0..self.nodes.len())
.filter(|&i| {
self.nodes[i]
.token
.as_ref()
.is_some_and(|t| t.annot.is_some())
})
.map(|i| NodeId(i as u64))
.collect();
let key = |nodes: &[Node], t: NodeId, name: &str| -> Option<String> {
nodes[t.0 as usize]
.token
.as_ref()?
.annot
.as_ref()?
.key(name)
.map(str::to_string)
};
let sentence_of = |nodes: &[Node], t: NodeId| -> u32 {
nodes[t.0 as usize].token.as_ref().map_or(0, |t| t.sentence)
};
let mut open: Option<(usize, Option<String>, u32)> = None;
for &t in &tokens {
let Some(tag) = key(&self.nodes, t, "ner") else {
open = None;
continue;
};
let (prefix, etype) = match tag.split_once('-') {
Some((p, e)) => (p.to_string(), Some(e.to_string())),
None => (tag.clone(), None),
};
let sentence = sentence_of(&self.nodes, t);
let continues = matches!(prefix.as_str(), "I" | "E")
&& open
.as_ref()
.is_some_and(|(_, ty, s)| *ty == etype && *s == sentence);
match prefix.as_str() {
"B" | "S" | "I" | "E" => {
if continues {
let (idx, _, _) = open.as_ref().unwrap();
self.mentions[*idx].tokens.push(t);
} else {
self.mentions.push(Mention {
tokens: vec![t],
etype: etype.clone(),
cluster: None,
});
}
let idx = if continues {
open.as_ref().unwrap().0
} else {
self.mentions.len() - 1
};
open = if matches!(prefix.as_str(), "B" | "I") {
Some((idx, etype, sentence))
} else {
None
};
}
_ => open = None,
}
}
let order: Vec<String> = entity_header
.unwrap_or("eid-etype-head-other")
.split('-')
.map(str::to_string)
.collect();
let mut stack: Vec<(String, usize)> = Vec::new();
for &t in &tokens {
let Some(spec) = key(&self.nodes, t, "Entity") else {
for (_, idx) in &stack {
if self.mentions[*idx].tokens.last() != Some(&t) {
self.mentions[*idx].tokens.push(t);
}
}
continue;
};
let mut closings: Vec<String> = Vec::new();
for entry in entity_entries(&spec) {
let entry = entry.as_str();
let opening = entry.starts_with('(');
let closing = entry.ends_with(')');
let body = entry.trim_start_matches('(').trim_end_matches(')');
if opening {
let mut attrs = body.splitn(order.len(), '-');
let eid = attrs.next().unwrap_or("").to_string();
let mut etype = None;
for (name, val) in order.iter().skip(1).zip(attrs) {
if name == "etype" && !val.is_empty() {
etype = Some(val.to_string());
}
}
let cluster = eid.split('[').next().unwrap_or(&eid).to_string();
let idx = self.mentions.len();
self.mentions.push(Mention {
tokens: vec![t],
etype,
cluster: Some(cluster),
});
if !closing {
stack.push((eid, idx));
}
} else if closing {
closings.push(body.to_string());
}
}
for (_, idx) in &stack {
if self.mentions[*idx].tokens.last() != Some(&t) {
self.mentions[*idx].tokens.push(t);
}
}
for eid in closings {
if let Some(pos) = stack.iter().rposition(|(e, _)| *e == eid) {
stack.remove(pos);
}
}
}
for (i, m) in self.mentions.iter().enumerate() {
for &t in &m.tokens {
if let Some(a) = self.nodes[t.0 as usize]
.token
.as_mut()
.and_then(|t| t.annot.as_mut())
{
a.mentions.push(i as u32 + 1);
}
}
}
}
fn mention_text(&self, m: &Mention) -> String {
let mut out = String::new();
for (i, &t) in m.tokens.iter().enumerate() {
let n = &self.nodes[t.0 as usize];
if i > 0 {
let prev = &self.nodes[m.tokens[i - 1].0 as usize];
let glued = prev
.token
.as_ref()
.and_then(|t| t.annot.as_ref())
.and_then(|a| a.key("SpaceAfter"))
== Some("No");
if !glued {
out.push(' ');
}
}
out.push_str(&n.prose);
}
out
}
fn token_field(&self, node: NodeId, name: &str) -> Option<Value> {
let t = self.nodes[node.0 as usize].token.as_ref()?;
if let Some(v) = field_values(&self.nodes[node.0 as usize].fields, name) {
return Some(v);
}
for &s in t.spans.iter().rev() {
if let Some(v) = field_values(&self.nodes[s.0 as usize].fields, name) {
return Some(v);
}
}
let mut at = self.nodes[node.0 as usize].parent;
while let Some(p) = at {
let n = &self.nodes[p.0 as usize];
if !(n.kind == Kind::Sentence && n.derived) {
return field_values(&n.fields, name);
}
at = n.parent;
}
None
}
fn mention_values(&self, node: NodeId, f: impl Fn(&Mention) -> Option<Value>) -> Option<Value> {
let a = self.nodes[node.0 as usize].token.as_ref()?.annot.as_ref()?;
let vs: Vec<Value> = a
.mentions
.iter()
.filter_map(|&i| f(&self.mentions[i as usize - 1]))
.collect();
match vs.len() {
0 => None,
1 => vs.into_iter().next(),
_ => Some(Value::list(vs)),
}
}
}
fn enclosing_span(nodes: &[Node], flow: NodeId, lo: u32, hi: u32) -> Option<NodeId> {
let mut best: Option<NodeId> = None;
let mut at = flow;
loop {
let next = nodes[at.0 as usize].children.iter().rev().find_map(|&c| {
let n = &nodes[c.0 as usize];
match (n.kind, n.at) {
(Kind::Span, Some((slo, shi)))
if slo <= lo && hi <= shi && (slo, shi) != (lo, hi) =>
{
Some(c)
}
_ => None,
}
});
match next {
Some(c) => {
best = Some(c);
at = c;
}
None => return best,
}
}
}
fn field_values(fields: &[(String, String)], name: &str) -> Option<Value> {
let vs: Vec<Value> = fields
.iter()
.filter(|(k, _)| k == name)
.map(|(_, v)| Value::Str(v.clone()))
.collect();
match vs.len() {
0 => None,
1 => vs.into_iter().next(),
_ => Some(Value::list(vs)),
}
}
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 if matches!(nodes[i].kind, Kind::Paragraph | Kind::Dialogue) {
} 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 | Kind::Span | Kind::Milestone
) {
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 if nodes[i].kind == Kind::Paragraph
&& !child_parts.is_empty()
&& nodes[i]
.children
.iter()
.all(|c| nodes[c.0 as usize].kind == Kind::Sentence)
{
parts.push(child_parts.join(" "));
} 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> {
let n = &self.nodes[node.0 as usize];
if n.kind == Kind::Span {
return n.span_kind.clone();
}
n.kind.name().map(str::to_string)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.nodes[node.0 as usize].parent
}
fn descendants_where(
&self,
node: NodeId,
name: &str,
property: &str,
value: &Value,
) -> Option<Vec<(NodeId, usize)>> {
if name != "token" || property != "lower" {
return None;
}
let Value::Str(word) = value else {
return None;
};
if word.is_empty() || !word.chars().all(|c| c.is_alphabetic() || "’'-".contains(c)) {
return None;
}
let index = self.token_index.get_or_init(|| {
let mut map: std::collections::HashMap<String, Vec<NodeId>> =
std::collections::HashMap::new();
for (i, n) in self.nodes.iter().enumerate() {
if n.token.is_some() {
map.entry(self.lower(&n.prose))
.or_default()
.push(NodeId(i as u64));
}
}
map
});
let Some(hits) = index.get(word.as_str()) else {
return Some(Vec::new());
};
let mut out = Vec::new();
for &t in hits {
let mut depth = 0usize;
let mut cur = t;
while let Some(p) = self.nodes[cur.0 as usize].parent {
depth += 1;
if p == node {
out.push((t, depth));
break;
}
cur = p;
}
}
Some(out)
}
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 {
for (ns, t) in self
.head
.tags
.iter()
.map(|t| ("tag", t))
.chain(self.head.category.iter().map(|c| ("category", c)))
{
let name = HeadMeta::trait_name(t);
if name.is_empty() {
continue;
}
let name = format!("{ns}:{name}");
if !out.contains(&name) {
out.push(name);
}
}
}
if let Some(t) = &n.token {
if t.annot.as_ref().is_some_and(|a| a.empty) {
out.push("empty".to_string());
} else {
out.push(t.class.name().to_string());
if t.class == TokenClass::Punct && is_quote(&n.prose) {
out.push("quote".to_string());
}
}
}
if n.kind != Kind::Document
&& !n.deixis
&& !n.derived
&& !matches!(
n.kind,
Kind::IndexMark
| Kind::Ref
| Kind::Cit
| Kind::Anchor
| Kind::Strophe
| Kind::Stichos
| Kind::Token
| Kind::Span
| Kind::Milestone
| Kind::Character
| Kind::Place
)
{
out.push("block".to_string());
}
for g in &n.genoses {
if !out.contains(g) {
out.push(g.clone());
}
}
if n.derived {
out.push("derived".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
| Kind::Span
| Kind::Milestone
))
{
out.push("target".to_string());
}
if n.kind == Kind::Cit && n.locator.is_some() {
out.push("locator".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 name == "cite" {
return self.cite(node).map(Value::Str);
}
if n.kind == Kind::Document {
let h = &self.head;
match name {
"title" | "lemma" => return h.title.clone().map(Value::Str),
"description" => return h.description.clone().map(Value::Str),
"author" => return h.author.clone().map(Value::Str),
"category" => return h.category.clone().map(Value::Str),
"tags" => {
return (!h.tags.is_empty())
.then(|| Value::list(h.tags.iter().cloned().map(Value::Str).collect()));
}
"published" => return h.published.clone(),
"modified" => return h.modified.clone(),
_ => {}
}
}
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 {
"href" => return self.href(node).map(Value::Str),
"anchor" => return self.nearest_onym(node).map(|(a, _)| Value::Str(a)),
"link" => {
let href = self.href(node)?;
let text = match self.default_value(node) {
Some(Value::Str(t)) => t,
_ => String::new(),
};
return Some(Value::Record(vec![
(
"title".to_string(),
self.document_title().map(Value::Str).unwrap_or(Value::Null),
),
("href".to_string(), Value::Str(href)),
("text".to_string(), Value::Str(text)),
]));
}
_ => {}
}
match name {
"lemma" | "upos" | "xpos" | "feats" | "deprel" | "deps" | "misc" | "id" | "mwt"
if n.token.as_ref().is_some_and(|t| t.annot.is_some()) =>
{
let a = n.token.as_ref()?.annot.as_ref()?;
match name {
"lemma" => a.lemma.clone(),
"upos" => a.upos.clone(),
"xpos" => a.xpos.clone(),
"feats" => a.feats.clone(),
"deprel" => a.deprel.clone(),
"deps" => a.deps.clone(),
"misc" => a.misc.clone(),
"id" => Some(a.id.clone()),
_ => a.mwt.as_ref().map(|(_, form)| form.clone()),
}
.map(Value::Str)
}
"id" if n.kind == Kind::Sentence => n
.fields
.iter()
.find(|(k, _)| k == "sent_id")
.map(|(_, v)| Value::Str(v.clone())),
"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),
"locator" if n.kind == Kind::Cit => n.locator.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())),
"lower" if n.token.is_some() => Some(Value::Str(self.lower(&n.prose))),
"modern" if n.token.is_some() => Some(Value::Str(self.modern(&n.prose))),
"class" if n.token.is_some() => n
.token
.as_ref()
.map(|t| Value::Str(t.class.name().to_string())),
"sentence" if n.token.is_some() => {
let parent = &self.nodes[n.parent?.0 as usize];
if parent.kind == Kind::Sentence {
return Some(Value::Str(parent.prose.clone()));
}
let t = n.token.as_ref()?;
let (lo, hi) = (t.span.0 as usize, t.span.1 as usize);
parent
.prose
.get(lo..hi)
.map(|s| Value::Str(s.trim().to_string()))
}
"mention" if n.token.is_some() => {
self.mention_values(node, |m| Some(Value::Str(self.mention_text(m))))
}
"entity" if n.token.is_some() => {
self.mention_values(node, |m| m.etype.clone().map(Value::Str))
}
_ if n.token.as_ref().is_some_and(|t| t.annot.is_some()) => n
.token
.as_ref()?
.annot
.as_ref()?
.key(name)
.map(|v| Value::Str(v.to_string()))
.or_else(|| self.token_field(node, name)),
_ if n.kind == Kind::Sentence => n
.fields
.iter()
.find(|(k, _)| k == name)
.or_else(|| n.fields.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)))
.map(|(_, v)| Value::Str(v.clone())),
_ if matches!(
n.kind,
Kind::Span
| Kind::Speech
| Kind::Dialogue
| Kind::Paragraph
| Kind::Milestone
| Kind::Character
| Kind::Place
) && !n.fields.is_empty() =>
{
field_values(&n.fields, name)
}
_ if n.token.is_some() => self.token_field(node, name),
_ => 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];
let character: Vec<(String, NodeId)> = n
.character
.map(|c| ("character".to_string(), c))
.into_iter()
.chain(n.place.map(|p| ("place".to_string(), p)))
.collect();
if let Some(a) = n.token.as_ref().and_then(|t| t.annot.as_ref()) {
let mut out = Vec::new();
if let Some(h) = a.head {
out.push(("head".to_string(), h));
}
for (t, _) in &a.eheads {
out.push(("ehead".to_string(), *t));
}
out.extend(character);
return out;
}
if !character.is_empty() && n.kind != Kind::Span {
return character;
}
if let Some(t) = n.ref_edge {
let label = if n.kind == Kind::Cit { "cit" } else { "ref" };
return vec![(label.to_string(), t)];
}
if n.kind == Kind::Span && !(n.span_tokens.is_empty() && character.is_empty()) {
return n
.span_tokens
.iter()
.map(|&t| ("token".to_string(), t))
.chain(character)
.collect();
}
match n.note_edge {
Some(body) => vec![(n.kind.name().unwrap_or("footnote").to_string(), body)],
None => Vec::new(),
}
}
fn link_property(
&self,
source: NodeId,
label: &str,
target: NodeId,
name: &str,
) -> Option<Value> {
if name != "rel" {
return None;
}
let a = self.nodes[source.0 as usize]
.token
.as_ref()?
.annot
.as_ref()?;
match label {
"head" if a.head == Some(target) => a.deprel.clone().map(Value::Str),
"ehead" => a
.eheads
.iter()
.find(|(t, _)| *t == target)
.map(|(_, r)| Value::Str(r.clone())),
_ => None,
}
}
fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
let n = &self.nodes[node.0 as usize];
if let Some(a) = n.token.as_ref().and_then(|t| t.annot.as_ref()) {
return a
.dependents
.iter()
.map(|&d| ("head".to_string(), d))
.chain(a.edependents.iter().map(|(d, _)| ("ehead".to_string(), *d)))
.collect();
}
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)));
let mention_label = if n.kind == Kind::Place {
"place"
} else {
"character"
};
out.extend(n.mentions.iter().map(|&m| (mention_label.to_string(), m)));
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
.link_base
.as_ref()
.or(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", "n"]
}
fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
let n = &self.nodes[node.0 as usize];
if n.kind == Kind::Document
&& let Some(v) = self.head.declared_value(key)
{
return Some(v);
}
match key {
"level" => n.level.map(|l| Value::Int(l as i64)),
"lang" => n
.lang
.clone()
.or_else(|| {
let i = n.genoses.iter().position(|g| g == "foreign")?;
n.genoses.get(i + 1).cloned()
})
.map(Value::Str),
"at" if n.at.is_some() => n.at.map(|(lo, _)| Value::Int(lo as i64)),
"n" | "sentence" if n.kind == Kind::Sentence => n.taxis.map(Value::Int),
"n" => n
.token
.as_ref()
.filter(|t| t.n > 0)
.map(|t| Value::Int(t.n as i64)),
"sentence" => n.token.as_ref().map(|t| Value::Int(t.sentence as i64)),
"mention" if n.token.is_some() => {
let ords: Vec<Value> = n
.token
.as_ref()
.and_then(|t| t.annot.as_ref())
.map(|a| a.mentions.iter().map(|&i| Value::Int(i as i64)).collect())
.unwrap_or_default();
match ords.len() {
0 => None,
1 => ords.into_iter().next(),
_ => Some(Value::list(ords)),
}
}
"entity" if n.token.is_some() => {
self.mention_values(node, |m| m.cluster.clone().map(Value::Str))
}
"mwt" => n
.token
.as_ref()
.and_then(|t| t.annot.as_ref())
.and_then(|a| a.mwt.as_ref())
.map(|(id, _)| Value::Str(id.clone())),
"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,
}
}
}