1use quarb::{AstAdapter, NodeId, Value};
53
54pub mod render;
55pub use render::{Render, render_node, render_nodes};
56
57#[derive(Debug, Clone, PartialEq)]
61pub enum Block {
62 Heading { level: u8, lemma: String },
65 Paragraph { text: String },
67 Dialogue {
72 lemma: String,
73 text: String,
74 lit: bool,
78 },
79 Speech {
85 text: String,
86 genoses: Vec<String>,
87 fields: Vec<(String, String)>,
88 },
89 Span {
97 kind: String,
98 lo: u32,
99 hi: u32,
100 genoses: Vec<String>,
101 fields: Vec<(String, String)>,
102 },
103 Milestone {
107 scheme: String,
108 value: String,
109 at: u32,
110 },
111 Annotate { fields: Vec<(String, String)> },
114 Parsed {
121 lo: u32,
122 hi: u32,
123 onym: Option<String>,
124 parsings: Vec<Parsing>,
125 },
126 Periodos { lo: u32, hi: u32, id: String },
130 Character {
135 lemma: String,
136 onym: Option<String>,
137 text: String,
138 genoses: Vec<String>,
139 },
140 Wear { genoses: Vec<String> },
145 Text { text: String },
149 Open {
152 kind: Container,
153 lemma: Option<String>,
154 },
155 Close { hypograph: Option<String> },
158 Verbatim { lang: Option<String>, text: String },
161 NoteRef {
169 onym: String,
170 family: Option<NoteFamily>,
171 margin: bool,
176 at: Option<u32>,
180 },
181 IndexMark { term: String },
185 Ref {
194 target: String,
195 text: Option<String>,
196 internal: bool,
197 },
198 Anchor { onym: String },
203 Cite {
210 target: String,
211 locator: Option<String>,
212 },
213 Bib {
221 key: String,
222 text: String,
223 fields: Vec<(String, String)>,
224 genus: Option<String>,
225 },
226 Label { onym: String },
231 Verse {
237 lemma: Option<String>,
238 strophes: Vec<Vec<String>>,
239 hypograph: Option<String>,
240 },
241 Table {
248 lemma: Option<String>,
249 headers: Option<Vec<String>>,
250 rows: Vec<Vec<Cell>>,
251 },
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct Cell {
260 pub label: Option<String>,
261 pub text: String,
262}
263
264impl From<&str> for Cell {
265 fn from(text: &str) -> Self {
266 Cell {
267 label: None,
268 text: text.to_string(),
269 }
270 }
271}
272
273impl From<String> for Cell {
274 fn from(text: String) -> Self {
275 Cell { label: None, text }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
281pub enum Container {
282 Blockquote,
283 UnorderedList,
284 OrderedList {
286 start: i64,
287 },
288 Item,
290 Note {
295 onym: String,
296 family: NoteFamily,
297 margin: bool,
298 },
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
308pub enum NoteFamily {
309 Footnote,
310 Endnote,
311 Aside,
312}
313
314impl NoteFamily {
315 fn kind(self) -> Kind {
316 match self {
317 NoteFamily::Footnote => Kind::Footnote,
318 NoteFamily::Endnote => Kind::Endnote,
319 NoteFamily::Aside => Kind::Aside,
320 }
321 }
322}
323
324#[derive(Debug, Clone, Default, PartialEq, Eq)]
330pub struct Parsing {
331 pub lexema: Option<String>,
332 pub meros: Option<String>,
333 pub parepomena: Option<String>,
334 pub semasia: Option<String>,
335 pub kephale: Option<String>,
336 pub schesis: Option<String>,
337}
338
339#[derive(Debug, Clone)]
342struct DeclaredToken {
343 lo: u32,
344 hi: u32,
345 onym: Option<String>,
346 parsings: Vec<Parsing>,
347}
348
349#[derive(Debug, Clone)]
354pub struct CastRow {
355 pub id: String,
356 pub name: String,
357 pub pattern: Option<regex::Regex>,
358 pub words: usize,
361 pub fields: Vec<(String, String)>,
362}
363
364impl CastRow {
365 pub fn read_csv(path: &str) -> Result<Vec<CastRow>, String> {
372 let mut reader = csv::Reader::from_path(path).map_err(|e| e.to_string())?;
373 let headers: Vec<String> = reader
374 .headers()
375 .map_err(|e| e.to_string())?
376 .iter()
377 .map(str::to_string)
378 .collect();
379 if !headers.iter().any(|h| h == "id") {
380 return Err(format!(
381 "a cast table needs an id column (found: {})",
382 headers.join(", ")
383 ));
384 }
385 let mut rows = Vec::new();
386 for record in reader.records() {
387 let record = record.map_err(|e| e.to_string())?;
388 let get = |k: &str| {
389 headers
390 .iter()
391 .position(|h| h == k)
392 .and_then(|i| record.get(i))
393 .map(str::trim)
394 .filter(|v| !v.is_empty())
395 };
396 let Some(id) = get("id") else { continue };
397 let (pattern, words) = if let Some(p) = get("pattern") {
398 let re = regex::Regex::new(p).map_err(|e| format!("cast pattern for {id}: {e}"))?;
399 (Some(re), p.split_whitespace().count().max(1))
402 } else if let Some(stem) = get("stem").or_else(|| get("prefix")) {
403 let re = regex::Regex::new(&format!("^{}", regex::escape(stem))).unwrap();
404 (Some(re), stem.split_whitespace().count())
405 } else if let Some(form) = get("mention").or_else(|| get("form")) {
406 let re = regex::Regex::new(&format!("^{}$", regex::escape(form))).unwrap();
407 (Some(re), form.split_whitespace().count())
408 } else {
409 (None, 0)
410 };
411 let fields: Vec<(String, String)> = headers
412 .iter()
413 .zip(record.iter())
414 .filter(|(h, v)| h.as_str() != "id" && !v.trim().is_empty())
415 .map(|(h, v)| (h.clone(), v.trim().to_string()))
416 .collect();
417 rows.push(CastRow {
418 id: id.to_string(),
419 name: get("name").unwrap_or(id).to_string(),
420 pattern,
421 words,
422 fields,
423 });
424 }
425 Ok(rows)
426 }
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
431enum Kind {
432 Document,
433 Section,
434 Character,
438 Place,
442 Paragraph,
443 Blockquote,
444 UnorderedList,
445 OrderedList,
446 UnorderedItem,
447 OrderedItem,
448 Verbatim,
449 Footnote,
453 Endnote,
455 Aside,
459 IndexMark,
461 Ref,
463 Cit,
466 Bib,
469 Anchor,
471 Verse,
473 Strophe,
475 Stichos,
477 Token,
486 Sentence,
495 Speech,
498 Dialogue,
501 Span,
504 Milestone,
506}
507
508impl Kind {
509 fn name(self) -> Option<&'static str> {
510 Some(match self {
511 Kind::Document => return None,
512 Kind::Section => "section",
513 Kind::Paragraph => "paragraph",
514 Kind::Blockquote => "blockquote",
515 Kind::Footnote => "footnote",
516 Kind::Endnote => "endnote",
517 Kind::Aside => "aside",
518 Kind::IndexMark => "index-mark",
519 Kind::Ref => "ref",
520 Kind::Cit => "cit",
521 Kind::Bib => "bib",
522 Kind::Anchor => "anchor",
523 Kind::Verse => "verse",
524 Kind::Strophe => "strophe",
525 Kind::Stichos => "stichos",
526 Kind::UnorderedList => "unordered-list",
527 Kind::OrderedList => "ordered-list",
528 Kind::UnorderedItem => "unordered-item",
529 Kind::OrderedItem => "ordered-item",
530 Kind::Verbatim => "verbatim",
531 Kind::Token => "token",
532 Kind::Sentence => "sentence",
533 Kind::Speech => "dialogue-line",
534 Kind::Character => "character",
535 Kind::Place => "place",
536 Kind::Dialogue => "dialogue",
537 Kind::Span => "span",
539 Kind::Milestone => "milestone",
540 })
541 }
542}
543
544struct Node {
545 kind: Kind,
546 lemma: Option<String>,
547 hypograph: Option<String>,
548 taxis: Option<i64>,
549 level: Option<u8>,
551 lang: Option<String>,
553 start: i64,
556 text: String,
558 prose: String,
560 table: bool,
562 row: bool,
564 cell: bool,
566 onym: Option<String>,
569 deixis: bool,
571 dangling: bool,
573 family_open: bool,
576 margin: bool,
578 note_edge: Option<NodeId>,
580 cites: Vec<NodeId>,
582 target: Option<String>,
584 locator: Option<String>,
586 internal: bool,
588 fields: Vec<(String, String)>,
591 genus: Option<String>,
592 ref_edge: Option<NodeId>,
594 ref_cites: Vec<NodeId>,
596 parent: Option<NodeId>,
597 children: Vec<NodeId>,
598 token: Option<TokenInfo>,
600 derived: bool,
604 genoses: Vec<String>,
608 span_kind: Option<String>,
609 at: Option<(u32, u32)>,
610 span_tokens: Vec<NodeId>,
611 character: Option<NodeId>,
614 mentions: Vec<NodeId>,
615 place: Option<NodeId>,
618 declared_tokens: Vec<DeclaredToken>,
621 declared_sentences: Vec<(u32, u32, String)>,
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq)]
626enum TokenClass {
627 Word,
629 Number,
631 Punct,
633}
634
635impl TokenClass {
636 fn name(self) -> &'static str {
637 match self {
638 TokenClass::Word => "word",
639 TokenClass::Number => "number",
640 TokenClass::Punct => "punct",
641 }
642 }
643}
644
645#[derive(Debug, Clone)]
646struct TokenInfo {
647 class: TokenClass,
648 n: u32,
651 sentence: u32,
653 span: (u32, u32),
656 annot: Option<Box<Annot>>,
658 at: u32,
661 spans: Vec<NodeId>,
662}
663
664#[derive(Debug, Clone, Default)]
674struct Annot {
675 id: String,
676 lemma: Option<String>,
677 upos: Option<String>,
678 xpos: Option<String>,
679 feats: Option<String>,
680 deprel: Option<String>,
681 deps: Option<String>,
682 misc: Option<String>,
683 keys: Vec<(String, String)>,
685 mwt: Option<(String, String)>,
688 empty: bool,
690 head: Option<NodeId>,
691 dependents: Vec<NodeId>,
692 eheads: Vec<(NodeId, String)>,
693 edependents: Vec<(NodeId, String)>,
694 mentions: Vec<u32>,
697}
698
699#[derive(Debug, Clone)]
705struct Mention {
706 tokens: Vec<NodeId>,
707 etype: Option<String>,
709 cluster: Option<String>,
711}
712
713impl Annot {
714 fn key(&self, name: &str) -> Option<&str> {
717 self.keys
718 .iter()
719 .find(|(k, _)| k == name)
720 .or_else(|| self.keys.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)))
721 .map(|(_, v)| v.as_str())
722 }
723}
724
725fn entity_entries(spec: &str) -> Vec<String> {
730 let mut out = Vec::new();
731 for piece in spec.split('|') {
732 let mut cur = String::new();
733 for ch in piece.chars() {
734 if ch == '(' && !cur.is_empty() {
735 out.push(std::mem::take(&mut cur));
736 }
737 cur.push(ch);
738 if ch == ')' {
739 out.push(std::mem::take(&mut cur));
740 }
741 }
742 if !cur.is_empty() {
743 out.push(cur);
744 }
745 }
746 out
747}
748
749fn key_values(column: &str) -> Vec<(String, String)> {
752 if column == "_" || column.is_empty() {
753 return Vec::new();
754 }
755 column
756 .split('|')
757 .filter(|e| !e.is_empty())
758 .map(|e| match e.split_once('=') {
759 Some((k, v)) => (k.to_string(), v.to_string()),
760 None => (e.to_string(), String::new()),
761 })
762 .collect()
763}
764
765fn sentence_spans(prose: &str, bonds: Option<&syndesmos::Syndesmos>) -> Vec<(usize, usize)> {
770 let raw: Vec<(usize, &str)> = match bonds {
771 Some(b) => b.split_sentence_bound_indices(prose),
772 None => {
773 use unicode_segmentation::UnicodeSegmentation;
774 prose.split_sentence_bound_indices().collect()
775 }
776 };
777 raw.into_iter()
778 .filter(|(_, seg)| !seg.trim().is_empty())
779 .map(|(at, seg)| (at, at + seg.len()))
780 .collect()
781}
782
783const CONLLU_COLUMNS: [&str; 10] = [
785 "ID", "FORM", "LEMMA", "UPOS", "XPOS", "FEATS", "HEAD", "DEPREL", "DEPS", "MISC",
786];
787
788#[derive(Debug, Clone, Default)]
790struct ConlluToken {
791 id: String,
792 range: Option<(u32, u32)>,
794 ord: u32,
796 empty: bool,
798 form: String,
799 lemma: String,
800 upos: String,
801 xpos: String,
802 feats: String,
803 head: String,
804 deprel: String,
805 deps: String,
806 misc: String,
807 extra: Vec<(String, String)>,
810}
811
812#[derive(Debug, Clone, Default)]
814struct ConlluSentence {
815 comments: Vec<(String, String)>,
817 newdoc: Option<Option<String>>,
819 newpar: Option<Option<String>>,
821 tokens: Vec<ConlluToken>,
822}
823
824#[derive(Debug, Clone, Default)]
827struct ConlluDoc {
828 sentences: Vec<ConlluSentence>,
829 globals: Vec<(String, String)>,
830}
831
832pub fn looks_like_conllu(text: &str) -> bool {
839 let mut columns = CONLLU_COLUMNS.len();
840 for line in text.lines().take(200) {
841 let line = line.trim_end_matches('\r');
842 if line.trim().is_empty() {
843 continue;
844 }
845 if let Some(c) = line.strip_prefix('#') {
846 if let Some((k, v)) = c.split_once('=')
847 && k.trim() == "global.columns"
848 {
849 columns = v.split_whitespace().count().max(2);
850 }
851 continue;
852 }
853 let fields: Vec<&str> = line.split('\t').collect();
854 if fields.len() != columns {
855 return false;
856 }
857 let id = fields[0];
858 let (a, b) = match id.split_once(['-', '.']) {
859 Some((a, b)) => (a, Some(b)),
860 None => (id, None),
861 };
862 return a.parse::<u32>().is_ok() && b.is_none_or(|b| b.parse::<u32>().is_ok());
863 }
864 false
865}
866
867pub fn read_conllu_dir(dir: &std::path::Path) -> Result<Vec<(String, String)>, String> {
873 fn walk(
874 dir: &std::path::Path,
875 base: &std::path::Path,
876 out: &mut Vec<(String, String)>,
877 ) -> Result<(), String> {
878 let mut entries: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
879 .map_err(|e| format!("reading {}: {e}", dir.display()))?
880 .filter_map(|e| e.ok().map(|e| e.path()))
881 .filter(|p| {
882 !p.file_name()
883 .and_then(|n| n.to_str())
884 .is_some_and(|n| n.starts_with('.'))
885 })
886 .collect();
887 entries.sort();
888 for p in entries {
889 if p.is_dir() {
890 walk(&p, base, out)?;
891 } else if p.extension().and_then(|e| e.to_str()).is_some_and(|e| {
892 e.eq_ignore_ascii_case("conllu") || e.eq_ignore_ascii_case("conllup")
893 }) {
894 let text = std::fs::read_to_string(&p)
895 .map_err(|e| format!("reading {}: {e}", p.display()))?;
896 let text = text
897 .strip_prefix('\u{feff}')
898 .map(str::to_owned)
899 .unwrap_or(text);
900 let name = p
901 .strip_prefix(base)
902 .unwrap_or(&p)
903 .to_string_lossy()
904 .into_owned();
905 out.push((name, text));
906 }
907 }
908 Ok(())
909 }
910 let mut out = Vec::new();
911 walk(dir, dir, &mut out)?;
912 if out.is_empty() {
913 return Err(format!("{} holds no .conllu file", dir.display()));
914 }
915 Ok(out)
916}
917
918fn parse_conllu(text: &str) -> Result<ConlluDoc, String> {
926 let mut doc = ConlluDoc::default();
927 let mut current = ConlluSentence::default();
928 let mut columns: Vec<String> = CONLLU_COLUMNS.iter().map(|c| c.to_string()).collect();
929 let trimmed = |s: &str| s.trim().to_string();
930 for (i, line) in text.lines().enumerate() {
931 let line = line.trim_end_matches('\r');
932 if line.trim().is_empty() {
933 if !current.tokens.is_empty() {
934 doc.sentences.push(std::mem::take(&mut current));
935 }
936 continue;
937 }
938 if let Some(comment) = line.strip_prefix('#') {
939 let comment = comment.trim();
940 if let Some(rest) = comment.strip_prefix("newdoc") {
941 let id = rest
942 .trim()
943 .strip_prefix("id")
944 .and_then(|r| r.trim().strip_prefix('='));
945 current.newdoc = Some(id.map(trimmed));
946 } else if let Some(rest) = comment.strip_prefix("newpar") {
947 let id = rest
948 .trim()
949 .strip_prefix("id")
950 .and_then(|r| r.trim().strip_prefix('='));
951 current.newpar = Some(id.map(trimmed));
952 } else if let Some((k, v)) = comment.split_once('=') {
953 let (k, v) = (k.trim(), v.trim());
954 if let Some(g) = k.strip_prefix("global.") {
955 if g == "columns" {
956 columns = v.split_whitespace().map(|c| c.to_string()).collect();
957 }
958 doc.globals.push((g.to_string(), v.to_string()));
959 } else {
960 current.comments.push((k.to_string(), v.to_string()));
961 }
962 }
963 continue;
964 }
965 let f: Vec<&str> = line.split('\t').collect();
966 if f.len() < 2 {
967 return Err(format!(
968 "CoNLL-U line {}: expected tab-separated columns",
969 i + 1
970 ));
971 }
972 let mut tok = ConlluToken::default();
973 for (k, name) in columns.iter().enumerate() {
974 let v = f.get(k).copied().unwrap_or("_");
975 match name.to_ascii_uppercase().as_str() {
976 "ID" => tok.id = v.to_string(),
977 "FORM" => tok.form = v.to_string(),
978 "LEMMA" => tok.lemma = v.to_string(),
979 "UPOS" => tok.upos = v.to_string(),
980 "XPOS" => tok.xpos = v.to_string(),
981 "FEATS" => tok.feats = v.to_string(),
982 "HEAD" => tok.head = v.to_string(),
983 "DEPREL" => tok.deprel = v.to_string(),
984 "DEPS" => tok.deps = v.to_string(),
985 "MISC" => tok.misc = v.to_string(),
986 _ => {
987 if v != "_" && !v.is_empty() {
988 tok.extra.push((name.replace(':', "-"), v.to_string()));
989 }
990 }
991 }
992 }
993 if tok.id.is_empty() {
994 return Err(format!("CoNLL-U line {}: no token id", i + 1));
995 }
996 if let Some((a, b)) = tok.id.split_once('-') {
997 match (a.parse::<u32>(), b.parse::<u32>()) {
998 (Ok(a), Ok(b)) if a <= b => {
999 tok.range = Some((a, b));
1000 tok.ord = a;
1001 }
1002 _ => {
1003 return Err(format!(
1004 "CoNLL-U line {}: malformed multiword range {:?}",
1005 i + 1,
1006 tok.id
1007 ));
1008 }
1009 }
1010 } else if let Some((a, _)) = tok.id.split_once('.') {
1011 tok.empty = true;
1012 tok.ord = a.parse().map_err(|_| {
1013 format!(
1014 "CoNLL-U line {}: malformed empty-node id {:?}",
1015 i + 1,
1016 tok.id
1017 )
1018 })?;
1019 } else {
1020 tok.ord = tok
1021 .id
1022 .parse()
1023 .map_err(|_| format!("CoNLL-U line {}: malformed token id {:?}", i + 1, tok.id))?;
1024 }
1025 current.tokens.push(tok);
1026 }
1027 if !current.tokens.is_empty() {
1028 doc.sentences.push(current);
1029 }
1030 Ok(doc)
1031}
1032
1033fn conllu_surface(tokens: &[ConlluToken]) -> String {
1037 let mut out = String::new();
1038 let mut covered: u32 = 0; for t in tokens {
1040 if t.empty || (t.range.is_none() && t.ord <= covered) {
1041 continue;
1042 }
1043 if let Some((_, b)) = t.range {
1044 covered = b;
1045 }
1046 out.push_str(&t.form);
1047 let no_space = key_values(&t.misc)
1048 .iter()
1049 .any(|(k, v)| k.eq_ignore_ascii_case("SpaceAfter") && v == "No");
1050 if !no_space {
1051 out.push(' ');
1052 }
1053 }
1054 out.trim_end().to_string()
1055}
1056
1057fn class_of(seg: &str) -> TokenClass {
1058 if seg.chars().any(char::is_alphabetic) {
1059 TokenClass::Word
1060 } else if seg.chars().any(char::is_numeric) {
1061 TokenClass::Number
1062 } else {
1063 TokenClass::Punct
1064 }
1065}
1066
1067fn class_of_annotated(form: &str, upos: &str) -> TokenClass {
1072 match upos {
1073 "PUNCT" => TokenClass::Punct,
1074 "NUM" => TokenClass::Number,
1075 _ => class_of(form),
1076 }
1077}
1078
1079fn is_quote(form: &str) -> bool {
1084 !form.is_empty()
1085 && form.chars().all(|c| {
1086 matches!(
1087 c,
1088 '"' | '\'' | '`'
1089 | '\u{AB}' | '\u{BB}'
1090 | '\u{2018}'..='\u{201F}'
1091 | '\u{2039}' | '\u{203A}'
1092 | '\u{300C}'..='\u{300F}'
1093 | '\u{301D}'..='\u{301F}'
1094 | '\u{FF02}' | '\u{FF07}'
1095 )
1096 })
1097}
1098
1099impl Node {
1100 fn new(kind: Kind, parent: Option<NodeId>) -> Self {
1101 Node {
1102 kind,
1103 token: None,
1104 derived: false,
1105 lemma: None,
1106 hypograph: None,
1107 taxis: None,
1108 level: None,
1109 lang: None,
1110 start: 1,
1111 text: String::new(),
1112 prose: String::new(),
1113 table: false,
1114 row: false,
1115 cell: false,
1116 onym: None,
1117 deixis: false,
1118 dangling: false,
1119 family_open: false,
1120 margin: false,
1121 note_edge: None,
1122 cites: Vec::new(),
1123 target: None,
1124 locator: None,
1125 internal: false,
1126 fields: Vec::new(),
1127 genus: None,
1128 ref_edge: None,
1129 ref_cites: Vec::new(),
1130 parent,
1131 children: Vec::new(),
1132 genoses: Vec::new(),
1133 span_kind: None,
1134 at: None,
1135 span_tokens: Vec::new(),
1136 character: None,
1137 place: None,
1138 mentions: Vec::new(),
1139 declared_tokens: Vec::new(),
1140 declared_sentences: Vec::new(),
1141 }
1142 }
1143}
1144
1145fn resolve_refs(nodes: &mut [Node]) -> std::collections::HashMap<String, NodeId> {
1152 let mut onyms: std::collections::HashMap<String, NodeId> = std::collections::HashMap::new();
1153 let mut bibs: std::collections::HashMap<String, NodeId> = std::collections::HashMap::new();
1157 for (i, n) in nodes.iter().enumerate() {
1158 if n.deixis || matches!(n.kind, Kind::IndexMark | Kind::Ref | Kind::Cit) {
1159 continue;
1160 }
1161 if n.kind == Kind::Bib {
1162 if let Some(k) = &n.onym {
1163 bibs.entry(k.clone()).or_insert(NodeId(i as u64));
1164 }
1165 continue;
1166 }
1167 let bearer = n.kind == Kind::Anchor
1168 || (!matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
1169 && n.onym.is_some());
1170 if bearer && let Some(o) = &n.onym {
1171 onyms.entry(o.clone()).or_insert(NodeId(i as u64));
1172 }
1173 }
1174 for i in 0..nodes.len() {
1175 let (map, key) = match nodes[i].kind {
1176 Kind::Ref if nodes[i].internal => (
1177 &onyms,
1178 nodes[i]
1179 .target
1180 .as_deref()
1181 .map(|t| t.trim_start_matches('#').to_string())
1182 .unwrap_or_default(),
1183 ),
1184 Kind::Cit => (&bibs, nodes[i].target.clone().unwrap_or_default()),
1185 _ => continue,
1186 };
1187 match map.get(&key) {
1188 Some(&bearer) => {
1189 nodes[i].ref_edge = Some(bearer);
1190 let me = NodeId(i as u64);
1191 nodes[bearer.0 as usize].ref_cites.push(me);
1192 }
1193 None => nodes[i].dangling = true,
1194 }
1195 }
1196 onyms
1197}
1198
1199pub fn normalize_ws(s: &str) -> String {
1203 s.split_whitespace().collect::<Vec<_>>().join(" ")
1204}
1205
1206#[derive(Debug, Clone, Default)]
1222pub struct HeadMeta {
1223 pub declared: Vec<(String, Vec<String>)>,
1226 pub title: Option<String>,
1227 pub description: Option<String>,
1228 pub author: Option<String>,
1229 pub category: Option<String>,
1231 pub tags: Vec<String>,
1233 pub published: Option<Value>,
1234 pub modified: Option<Value>,
1235}
1236
1237impl HeadMeta {
1238 pub fn declare(&mut self, name: &str, value: &str) {
1240 let value = normalize_ws(value);
1241 if name.is_empty() || value.is_empty() {
1242 return;
1243 }
1244 match self.declared.iter_mut().find(|(k, _)| k == name) {
1245 Some((_, vs)) => vs.push(value),
1246 None => self.declared.push((name.to_string(), vec![value])),
1247 }
1248 }
1249
1250 pub fn tag(&mut self, tag: &str) {
1252 let tag = normalize_ws(tag);
1253 if tag.is_empty() || self.tags.iter().any(|t| t.eq_ignore_ascii_case(&tag)) {
1254 return;
1255 }
1256 self.tags.push(tag);
1257 }
1258
1259 pub fn declared_value(&self, name: &str) -> Option<Value> {
1262 let (_, vs) = self.declared.iter().find(|(k, _)| k == name)?;
1263 Some(match vs.as_slice() {
1264 [one] => Value::Str(one.clone()),
1265 many => Value::list(many.iter().cloned().map(Value::Str).collect()),
1266 })
1267 }
1268
1269 pub fn trait_name(s: &str) -> String {
1274 let mut out = String::new();
1275 let mut dash = false;
1276 for c in s.chars() {
1277 if c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '+') {
1278 out.push(c);
1279 dash = c == '-';
1280 } else if !dash && !out.is_empty() {
1281 out.push('-');
1282 dash = true;
1283 }
1284 }
1285 while out.ends_with('-') {
1286 out.pop();
1287 }
1288 out
1289 }
1290}
1291
1292pub struct TextModel {
1294 nodes: Vec<Node>,
1295 root: NodeId,
1296 onyms: std::collections::HashMap<String, NodeId>,
1301 document_url: Option<url::Url>,
1303 link_base: Option<url::Url>,
1307 document_path: Option<String>,
1311 cite_scheme: Option<String>,
1315 orthography: Option<String>,
1320 bib_aliases: std::collections::HashMap<String, String>,
1326 head: HeadMeta,
1329 token_index: std::sync::OnceLock<std::collections::HashMap<String, Vec<NodeId>>>,
1334 mentions: Vec<Mention>,
1337}
1338
1339impl TextModel {
1340 pub fn build(blocks: Vec<Block>) -> Self {
1347 let mut nodes = vec![Node::new(Kind::Document, None)];
1348 let root = NodeId(0);
1349 let mut sections: Vec<NodeId> = Vec::new();
1351 let mut containers: Vec<NodeId> = Vec::new();
1353 let mut last_flow: Option<NodeId> = None;
1356 let mut last_block: Option<NodeId> = None;
1361
1362 for block in blocks {
1363 match block {
1364 Block::Heading { level, lemma } => {
1365 let lemma = normalize_ws(&lemma);
1366 if !containers.is_empty() {
1367 if !lemma.is_empty() {
1370 let parent = *containers.last().unwrap();
1371 let id = push(&mut nodes, Kind::Paragraph, parent);
1372 nodes[id.0 as usize].text = lemma;
1373 }
1374 continue;
1375 }
1376 while let Some(&open) = sections.last() {
1377 if nodes[open.0 as usize].level >= Some(level) {
1378 sections.pop();
1379 } else {
1380 break;
1381 }
1382 }
1383 let parent = sections.last().copied().unwrap_or(root);
1384 let id = push(&mut nodes, Kind::Section, parent);
1385 let n = &mut nodes[id.0 as usize];
1386 n.lemma = Some(lemma);
1387 n.level = Some(level);
1388 sections.push(id);
1389 last_flow = Some(id);
1390 last_block = Some(id);
1391 }
1392 Block::Paragraph { text } => {
1393 let text = normalize_ws(&text);
1394 if text.is_empty() {
1395 continue;
1396 }
1397 let parent = cursor(§ions, &containers, root);
1398 let id = push(&mut nodes, Kind::Paragraph, parent);
1399 nodes[id.0 as usize].text = text;
1400 last_flow = Some(id);
1401 last_block = Some(id);
1402 }
1403 Block::Dialogue { lemma, text, lit } => {
1405 let text = normalize_ws(&text);
1406 if text.is_empty() {
1407 continue;
1408 }
1409 let parent = cursor(§ions, &containers, root);
1410 let kind = if lit { Kind::Dialogue } else { Kind::Paragraph };
1411 let id = push(&mut nodes, kind, parent);
1412 let n = &mut nodes[id.0 as usize];
1413 n.text = text;
1414 n.lemma = Some(normalize_ws(&lemma)).filter(|l| !l.is_empty());
1415 last_flow = Some(id);
1416 last_block = Some(id);
1417 }
1418 Block::Speech {
1420 text,
1421 genoses,
1422 fields,
1423 } => {
1424 let text = normalize_ws(&text);
1425 if text.is_empty() {
1426 continue;
1427 }
1428 let parent = cursor(§ions, &containers, root);
1429 let id = push(&mut nodes, Kind::Speech, parent);
1430 let n = &mut nodes[id.0 as usize];
1431 n.text = text;
1432 n.genoses = genoses;
1433 n.fields = fields;
1434 last_flow = Some(id);
1435 last_block = Some(id);
1436 }
1437 Block::Span {
1440 kind,
1441 lo,
1442 hi,
1443 genoses,
1444 fields,
1445 } => {
1446 let Some(flow) = last_flow else { continue };
1447 let text = nodes[flow.0 as usize]
1448 .text
1449 .get(lo as usize..hi as usize)
1450 .unwrap_or("")
1451 .to_string();
1452 let parent = enclosing_span(&nodes, flow, lo, hi).unwrap_or(flow);
1453 let id = push(&mut nodes, Kind::Span, parent);
1454 let n = &mut nodes[id.0 as usize];
1455 n.span_kind = Some(kind);
1456 n.at = Some((lo, hi));
1457 n.text = text;
1458 n.genoses = genoses;
1459 n.fields = fields;
1460 }
1461 Block::Character {
1462 lemma,
1463 onym,
1464 text,
1465 genoses,
1466 } => {
1467 let parent = cursor(§ions, &containers, root);
1468 let id = push(&mut nodes, Kind::Character, parent);
1469 let n = &mut nodes[id.0 as usize];
1470 n.lemma = Some(normalize_ws(&lemma)).filter(|l| !l.is_empty());
1471 n.onym = onym;
1472 n.text = normalize_ws(&text);
1473 n.genoses = genoses;
1474 last_block = Some(id);
1475 }
1476 Block::Parsed {
1477 lo,
1478 hi,
1479 onym,
1480 parsings,
1481 } => {
1482 let Some(flow) = last_flow else { continue };
1483 nodes[flow.0 as usize].declared_tokens.push(DeclaredToken {
1484 lo,
1485 hi,
1486 onym,
1487 parsings,
1488 });
1489 }
1490 Block::Periodos { lo, hi, id } => {
1491 let Some(flow) = last_flow else { continue };
1492 nodes[flow.0 as usize].declared_sentences.push((lo, hi, id));
1493 }
1494 Block::Milestone { scheme, value, at } => {
1495 let Some(flow) = last_flow else { continue };
1496 let at = if at == u32::MAX {
1500 nodes[flow.0 as usize].text.len() as u32
1501 } else {
1502 at
1503 };
1504 let parent = enclosing_span(&nodes, flow, at, at).unwrap_or(flow);
1505 let id = push(&mut nodes, Kind::Milestone, parent);
1506 let n = &mut nodes[id.0 as usize];
1507 n.at = Some((at, at));
1508 n.fields = vec![("scheme".to_string(), scheme), ("value".to_string(), value)];
1509 }
1510 Block::Annotate { fields } => {
1511 if let Some(b) = last_block {
1512 nodes[b.0 as usize].fields.extend(fields);
1513 }
1514 }
1515 Block::Wear { genoses } => {
1516 if let Some(b) = last_block {
1517 let own = &mut nodes[b.0 as usize].genoses;
1518 for g in genoses {
1519 if !own.contains(&g) {
1520 own.push(g);
1521 }
1522 }
1523 }
1524 }
1525 Block::Text { text } => {
1526 let text = normalize_ws(&text);
1527 if text.is_empty() {
1528 continue;
1529 }
1530 match containers.last() {
1531 Some(&open) => {
1532 let own = &mut nodes[open.0 as usize].text;
1533 if !own.is_empty() {
1534 own.push(' ');
1535 }
1536 own.push_str(&text);
1537 last_flow = Some(open);
1538 }
1539 None => {
1540 let parent = sections.last().copied().unwrap_or(root);
1541 let id = push(&mut nodes, Kind::Paragraph, parent);
1542 nodes[id.0 as usize].text = text;
1543 last_flow = Some(id);
1544 }
1545 }
1546 }
1547 Block::Open { kind, lemma } => {
1548 let kind = match kind {
1552 Container::Note {
1553 onym,
1554 family,
1555 margin,
1556 } => {
1557 let id = push(&mut nodes, family.kind(), root);
1558 let n = &mut nodes[id.0 as usize];
1559 n.onym = Some(onym.trim().to_string()).filter(|o| !o.is_empty());
1560 n.margin = margin;
1561 containers.push(id);
1562 last_flow = Some(id);
1563 continue;
1564 }
1565 other => other,
1566 };
1567 let parent = cursor(§ions, &containers, root);
1568 let (nkind, start) = match kind {
1569 Container::Blockquote => (Kind::Blockquote, None),
1570 Container::UnorderedList => (Kind::UnorderedList, None),
1571 Container::OrderedList { start } => (Kind::OrderedList, Some(start)),
1572 Container::Item => (
1573 match nodes[parent.0 as usize].kind {
1574 Kind::OrderedList => Kind::OrderedItem,
1575 _ => Kind::UnorderedItem,
1576 },
1577 None,
1578 ),
1579 Container::Note { .. } => unreachable!("handled above"),
1580 };
1581 let id = push(&mut nodes, nkind, parent);
1582 last_block = Some(id);
1583 nodes[id.0 as usize].lemma =
1584 lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
1585 if let Some(start) = start {
1586 nodes[id.0 as usize].start = start;
1587 }
1588 if nkind == Kind::OrderedItem {
1589 let nth = nodes[parent.0 as usize]
1592 .children
1593 .iter()
1594 .filter(|&&c| nodes[c.0 as usize].kind == Kind::OrderedItem)
1595 .count() as i64;
1596 let start = nodes[parent.0 as usize].start;
1597 nodes[id.0 as usize].taxis = Some(start + nth - 1);
1598 }
1599 containers.push(id);
1600 }
1601 Block::Close { hypograph } => {
1602 if let Some(open) = containers.pop() {
1603 nodes[open.0 as usize].hypograph = hypograph
1604 .map(|h| normalize_ws(&h))
1605 .filter(|h| !h.is_empty());
1606 }
1607 }
1608 Block::NoteRef {
1612 onym,
1613 family,
1614 margin,
1615 at,
1616 } => {
1617 let parent = last_flow.unwrap_or(root);
1618 let kind = family.map(NoteFamily::kind).unwrap_or(Kind::Footnote);
1622 let id = push(&mut nodes, kind, parent);
1623 let n = &mut nodes[id.0 as usize];
1624 n.deixis = true;
1625 n.family_open = family.is_none();
1626 n.margin = margin;
1627 n.at = at.map(|a| (a, a));
1628 n.onym = Some(onym.trim().to_string()).filter(|o| !o.is_empty());
1629 }
1630 Block::Ref {
1631 target,
1632 text,
1633 internal,
1634 } => {
1635 let parent = last_flow.unwrap_or(root);
1636 let id = push(&mut nodes, Kind::Ref, parent);
1637 let n = &mut nodes[id.0 as usize];
1638 n.internal = internal;
1639 n.target = Some(target.trim().to_string()).filter(|t| !t.is_empty());
1640 n.text = text.map(|t| normalize_ws(&t)).unwrap_or_default();
1641 }
1642 Block::Cite { target, locator } => {
1643 let parent = last_flow.unwrap_or(root);
1644 let id = push(&mut nodes, Kind::Cit, parent);
1645 let n = &mut nodes[id.0 as usize];
1646 n.internal = true;
1647 n.target = Some(target.trim().to_string()).filter(|t| !t.is_empty());
1648 n.locator = locator
1649 .map(|l| l.trim().to_string())
1650 .filter(|l| !l.is_empty());
1651 }
1652 Block::Bib {
1653 key,
1654 text,
1655 fields,
1656 genus,
1657 } => {
1658 let parent = cursor(§ions, &containers, root);
1659 let id = push(&mut nodes, Kind::Bib, parent);
1660 last_block = Some(id);
1661 let n = &mut nodes[id.0 as usize];
1662 n.onym = Some(key.trim().to_string()).filter(|k| !k.is_empty());
1663 n.text = normalize_ws(&text);
1664 n.fields = fields
1665 .into_iter()
1666 .map(|(k, v)| (k.trim().to_string(), normalize_ws(&v)))
1667 .filter(|(k, v)| !k.is_empty() && !v.is_empty())
1668 .collect();
1669 n.genus = genus
1670 .map(|g| g.trim().to_string())
1671 .filter(|g| !g.is_empty());
1672 }
1673 Block::Anchor { onym } => {
1674 let parent = last_flow.unwrap_or(root);
1675 let id = push(&mut nodes, Kind::Anchor, parent);
1676 nodes[id.0 as usize].onym =
1677 Some(onym.trim().to_string()).filter(|o| !o.is_empty());
1678 }
1679 Block::Label { onym } => {
1680 let onym = onym.trim().to_string();
1681 if onym.is_empty() {
1682 continue;
1683 }
1684 let free = last_block
1690 .map(|b| nodes[b.0 as usize].onym.is_none())
1691 .unwrap_or(false);
1692 match (last_block, free) {
1693 (Some(b), true) => {
1694 nodes[b.0 as usize].onym = Some(onym);
1695 }
1696 _ => {
1697 let parent = last_flow.unwrap_or(root);
1698 let id = push(&mut nodes, Kind::Anchor, parent);
1699 nodes[id.0 as usize].onym = Some(onym);
1700 }
1701 }
1702 }
1703 Block::IndexMark { term } => {
1704 let parent = last_flow.unwrap_or(root);
1705 let id = push(&mut nodes, Kind::IndexMark, parent);
1706 nodes[id.0 as usize].onym = Some(quarb_term(&term)).filter(|t| !t.is_empty());
1707 }
1708 Block::Verbatim { lang, text } => {
1709 let parent = cursor(§ions, &containers, root);
1710 let id = push(&mut nodes, Kind::Verbatim, parent);
1711 last_block = Some(id);
1712 let n = &mut nodes[id.0 as usize];
1713 n.lang = lang.filter(|l| !l.is_empty());
1714 n.text = text;
1715 }
1716 Block::Table {
1717 lemma,
1718 headers,
1719 rows,
1720 } => {
1721 let parent = cursor(§ions, &containers, root);
1722 lower_table(&mut nodes, parent, lemma, headers, rows);
1723 }
1724 Block::Verse {
1725 lemma,
1726 strophes,
1727 hypograph,
1728 } => {
1729 let parent = cursor(§ions, &containers, root);
1730 let id = lower_verse(&mut nodes, parent, lemma, strophes, hypograph);
1731 last_flow = Some(id);
1732 last_block = Some(id);
1733 }
1734 }
1735 }
1736
1737 resolve_notes(&mut nodes);
1738 let onyms = resolve_refs(&mut nodes);
1739 flatten_prose(&mut nodes);
1740 let mut model = TextModel {
1741 nodes,
1742 root,
1743 onyms,
1744 document_url: None,
1745 link_base: None,
1746 document_path: None,
1747 cite_scheme: None,
1748 orthography: None,
1749 bib_aliases: Default::default(),
1750 head: HeadMeta::default(),
1751 token_index: Default::default(),
1752 mentions: Vec::new(),
1753 };
1754 model.link_characters();
1755 model
1756 }
1757
1758 pub fn set_bib_aliases(&mut self, map: std::collections::HashMap<String, String>) {
1762 self.bib_aliases = map;
1763 }
1764
1765 pub fn set_document_url(&mut self, url: &str) {
1768 self.document_url = url::Url::parse(url).ok();
1769 }
1770
1771 pub fn set_link_base(&mut self, url: &str) {
1775 self.link_base = url::Url::parse(url).ok();
1776 }
1777
1778 pub fn set_document_path(&mut self, path: &str) {
1782 self.document_path = Some(path.to_string());
1783 }
1784
1785 pub fn set_cite_scheme(&mut self, scheme: &str) {
1788 self.cite_scheme = Some(scheme.to_string());
1789 }
1790
1791 pub fn set_orthography(&mut self, table: &str) -> Result<(), String> {
1794 quarb::translit::modernize("", table)?;
1795 self.orthography = Some(table.to_string());
1796 Ok(())
1797 }
1798
1799 fn modern(&self, form: &str) -> String {
1802 match &self.orthography {
1803 Some(t) => quarb::translit::modernize(form, t).unwrap_or_else(|_| form.to_string()),
1804 None => form.to_string(),
1805 }
1806 }
1807
1808 fn lower(&self, form: &str) -> String {
1810 self.modern(form).to_lowercase()
1811 }
1812
1813 fn cite_table(&self) -> Vec<(NodeId, u32, NodeId)> {
1816 let mut scheme = self.cite_scheme.clone();
1817 let mut out = Vec::new();
1818 for i in 0..self.nodes.len() {
1819 let n = &self.nodes[i];
1820 if n.kind != Kind::Milestone {
1821 continue;
1822 }
1823 let this = n
1824 .fields
1825 .iter()
1826 .find(|(k, _)| k == "scheme")
1827 .map(|(_, v)| v.clone());
1828 let Some(this) = this else { continue };
1829 match &scheme {
1830 None => scheme = Some(this),
1831 Some(s) if *s != this => continue,
1832 _ => {}
1833 }
1834 let at = n.at.map(|(a, _)| a).unwrap_or(0);
1835 let mut block = NodeId(i as u64);
1836 while self.nodes[block.0 as usize].kind == Kind::Span
1837 || self.nodes[block.0 as usize].kind == Kind::Milestone
1838 {
1839 match self.nodes[block.0 as usize].parent {
1840 Some(p) => block = p,
1841 None => break,
1842 }
1843 }
1844 out.push((block, at, NodeId(i as u64)));
1845 }
1846 out.sort_by_key(|(b, at, _)| (b.0, *at));
1847 out
1848 }
1849
1850 fn cite(&self, node: NodeId) -> Option<String> {
1858 let table = self.cite_table();
1859 if table.is_empty() {
1860 return self.structural_cite(node);
1861 }
1862 let n = &self.nodes[node.0 as usize];
1863 let value = |m: NodeId| {
1864 self.nodes[m.0 as usize]
1865 .fields
1866 .iter()
1867 .find(|(k, _)| k == "value")
1868 .map(|(_, v)| v.clone())
1869 };
1870 let container = matches!(n.kind, Kind::Document | Kind::Section)
1872 || (n.kind != Kind::Span
1873 && n.kind != Kind::Milestone
1874 && n.token.is_none()
1875 && n.kind != Kind::Sentence
1876 && !self.prose_blocks().contains(&node)
1877 && !n.children.is_empty()
1878 && n.prose.is_empty());
1879 if container {
1880 let inside = |mut b: NodeId| {
1881 loop {
1882 if b == node {
1883 return true;
1884 }
1885 match self.nodes[b.0 as usize].parent {
1886 Some(p) => b = p,
1887 None => return false,
1888 }
1889 }
1890 };
1891 return table
1892 .iter()
1893 .find(|(b, _, _)| inside(*b))
1894 .and_then(|(_, _, m)| value(*m));
1895 }
1896 let body = if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis {
1898 Some(node)
1899 } else {
1900 self.note_body_of(node)
1901 };
1902 if let Some(body) = body {
1903 let callout = (0..self.nodes.len())
1904 .find(|&i| self.nodes[i].deixis && self.nodes[i].note_edge == Some(body))
1905 .map(|i| NodeId(i as u64));
1906 return callout.and_then(|c| self.cite(c));
1907 }
1908 let (block, at) = match n.kind {
1910 _ if n.deixis && n.at.is_some() => {
1911 (n.parent.unwrap_or(node), n.at.map(|(a, _)| a).unwrap_or(0))
1912 }
1913 Kind::Milestone | Kind::Span => {
1914 let mut b = node;
1915 while matches!(self.nodes[b.0 as usize].kind, Kind::Span | Kind::Milestone) {
1916 match self.nodes[b.0 as usize].parent {
1917 Some(p) => b = p,
1918 None => break,
1919 }
1920 }
1921 (b, n.at.map(|(a, _)| a).unwrap_or(0))
1922 }
1923 _ if n.token.is_some() => {
1924 let mut b = node;
1925 while self.nodes[b.0 as usize].token.is_some()
1926 || self.nodes[b.0 as usize].kind == Kind::Sentence
1927 {
1928 match self.nodes[b.0 as usize].parent {
1929 Some(p) => b = p,
1930 None => break,
1931 }
1932 }
1933 (b, n.token.as_ref().map(|t| t.at).unwrap_or(0))
1934 }
1935 Kind::Sentence if n.derived => {
1936 let mut toks = Vec::new();
1937 self.collect_tokens(node, &mut toks);
1938 let at = toks
1939 .first()
1940 .and_then(|t| self.nodes[t.0 as usize].token.as_ref().map(|t| t.at))
1941 .unwrap_or(0);
1942 (n.parent.unwrap_or(node), at)
1943 }
1944 _ => (node, 0),
1945 };
1946 table
1947 .iter()
1948 .rev()
1949 .find(|(b, a, _)| (b.0, *a) <= (block.0, at))
1950 .and_then(|(_, _, m)| value(*m))
1951 .or_else(|| self.structural_cite(node))
1952 }
1953
1954 fn structural_cite(&self, node: NodeId) -> Option<String> {
1958 let mut names: Vec<String> = Vec::new();
1959 let mut cur = Some(node);
1960 while let Some(n) = cur {
1961 let node = &self.nodes[n.0 as usize];
1962 if node.kind == Kind::Section
1963 && let Some(l) = &node.lemma
1964 && !l.is_empty()
1965 {
1966 names.push(l.clone());
1967 }
1968 cur = node.parent;
1969 }
1970 if names.is_empty() {
1971 return None;
1972 }
1973 names.reverse();
1974 Some(names.join(" / "))
1975 }
1976
1977 fn href_base(&self) -> Option<String> {
1980 if let Some(u) = &self.document_url {
1981 return Some(u.to_string());
1982 }
1983 self.document_path.clone()
1984 }
1985
1986 fn nearest_onym(&self, node: NodeId) -> Option<(String, bool)> {
1990 let mut cur = Some(node);
1991 let mut exact = true;
1992 while let Some(id) = cur {
1993 let n = &self.nodes[id.0 as usize];
1994 if n.kind != Kind::Document
1995 && !n.deixis
1996 && let Some(o) = &n.onym
1997 {
1998 return Some((o.clone(), exact));
1999 }
2000 exact = false;
2001 cur = n.parent;
2002 }
2003 None
2004 }
2005
2006 fn href(&self, node: NodeId) -> Option<String> {
2018 let base = self.href_base()?;
2019 let n = &self.nodes[node.0 as usize];
2020 if n.kind == Kind::Document {
2021 return Some(base);
2022 }
2023 let served = base.split(['?', '#']).next().unwrap_or(&base);
2024 let last = served.rsplit('/').next().unwrap_or(served);
2025 let html = served.ends_with('/')
2026 || !last.contains('.')
2027 || last.ends_with(".html")
2028 || last.ends_with(".htm");
2029 if !html {
2030 return Some(base);
2031 }
2032 let mut out = base;
2033 let (anchor, exact) = match self.nearest_onym(node) {
2034 Some((a, exact)) => (Some(a), exact),
2035 None => (None, false),
2036 };
2037 if let Some(a) = &anchor {
2038 out.push('#');
2039 out.push_str(a);
2040 }
2041 if !exact {
2042 let words: Vec<&str> = n.prose.split_whitespace().collect();
2043 if !words.is_empty() {
2044 use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
2045 let enc =
2046 |ws: &[&str]| utf8_percent_encode(&ws.join(" "), NON_ALPHANUMERIC).to_string();
2047 if anchor.is_none() {
2048 out.push('#');
2049 }
2050 out.push_str(":~:text=");
2051 if words.len() <= 12 {
2052 out.push_str(&enc(&words));
2053 } else {
2054 out.push_str(&enc(&words[..6]));
2055 out.push(',');
2056 out.push_str(&enc(&words[words.len() - 6..]));
2057 }
2058 }
2059 }
2060 Some(out)
2061 }
2062
2063 fn document_title(&self) -> Option<String> {
2066 if let Some(t) = &self.head.title {
2067 return Some(t.clone());
2068 }
2069 self.nodes[self.root.0 as usize]
2070 .children
2071 .iter()
2072 .map(|c| &self.nodes[c.0 as usize])
2073 .find(|n| n.kind == Kind::Section)
2074 .and_then(|n| n.lemma.clone())
2075 }
2076
2077 pub fn set_head_meta(&mut self, head: HeadMeta) {
2080 self.head = head;
2081 }
2082
2083 pub fn head_meta(&self) -> &HeadMeta {
2085 &self.head
2086 }
2087
2088 pub fn plain_text(&self) -> String {
2095 let mut out = String::new();
2096 let mut stack = vec![self.root];
2097 while let Some(id) = stack.pop() {
2098 let n = &self.nodes[id.0 as usize];
2099 if let Some(l) = &n.lemma
2100 && !l.is_empty()
2101 {
2102 out.push_str(l);
2103 out.push('\n');
2104 }
2105 if !n.prose.is_empty() && n.kind != Kind::Document {
2109 out.push_str(&n.prose);
2110 out.push('\n');
2111 }
2112 for c in n.children.iter().rev() {
2113 stack.push(*c);
2114 }
2115 }
2116 out
2117 }
2118
2119 pub fn node_count(&self) -> usize {
2123 self.nodes.len()
2124 }
2125
2126 pub fn parse_plain(text: &str) -> Self {
2130 let mut blocks = Vec::new();
2131 let mut para: Vec<&str> = Vec::new();
2132 for line in text.lines() {
2133 if line.trim().is_empty() {
2134 if !para.is_empty() {
2135 blocks.push(Block::Paragraph {
2136 text: para.join(" "),
2137 });
2138 para.clear();
2139 }
2140 } else {
2141 para.push(line);
2142 }
2143 }
2144 if !para.is_empty() {
2145 blocks.push(Block::Paragraph {
2146 text: para.join(" "),
2147 });
2148 }
2149 Self::build(blocks)
2150 }
2151
2152 pub fn locator(&self, node: NodeId) -> String {
2156 let mut segments = Vec::new();
2157 let mut cur = Some(node);
2158 while let Some(id) = cur {
2159 let n = &self.nodes[id.0 as usize];
2160 if let Some(name) = n.kind.name() {
2161 segments.push(self.segment(id, name));
2162 }
2163 cur = n.parent;
2164 }
2165 segments.reverse();
2166 format!("/{}", segments.join("/"))
2167 }
2168
2169 fn segment(&self, node: NodeId, name: &str) -> String {
2170 let Some(parent) = self.nodes[node.0 as usize].parent else {
2171 return name.to_string();
2172 };
2173 let siblings = &self.nodes[parent.0 as usize].children;
2174 let same_name: Vec<NodeId> = siblings
2175 .iter()
2176 .copied()
2177 .filter(|&s| self.nodes[s.0 as usize].kind == self.nodes[node.0 as usize].kind)
2178 .collect();
2179 if same_name.len() > 1 {
2180 let n = same_name.iter().position(|&s| s == node).unwrap() + 1;
2181 format!("{name}[{n}]")
2182 } else {
2183 name.to_string()
2184 }
2185 }
2186}
2187
2188fn resolve_notes(nodes: &mut [Node]) {
2196 let mut bodies: std::collections::HashMap<(Kind, String), NodeId> = Default::default();
2197 for (i, n) in nodes.iter().enumerate() {
2198 if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
2199 && !n.deixis
2200 && let Some(o) = &n.onym
2201 {
2202 bodies
2203 .entry((n.kind, o.clone()))
2204 .or_insert(NodeId(i as u64));
2205 }
2206 }
2207 for i in 0..nodes.len() {
2208 if !matches!(nodes[i].kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
2209 || !nodes[i].deixis
2210 {
2211 continue;
2212 }
2213 let Some(onym) = nodes[i].onym.clone() else {
2214 nodes[i].dangling = true;
2215 continue;
2216 };
2217 let hit = if nodes[i].family_open {
2218 [Kind::Footnote, Kind::Endnote, Kind::Aside]
2221 .into_iter()
2222 .find_map(|k| bodies.get(&(k, onym.clone())).map(|b| (k, *b)))
2223 } else {
2224 bodies
2225 .get(&(nodes[i].kind, onym))
2226 .map(|b| (nodes[i].kind, *b))
2227 };
2228 match hit {
2229 Some((family, body)) => {
2230 nodes[i].kind = family;
2231 nodes[i].note_edge = Some(body);
2232 let callout = NodeId(i as u64);
2233 nodes[body.0 as usize].cites.push(callout);
2234 }
2235 None => nodes[i].dangling = true,
2236 }
2237 }
2238}
2239
2240fn quarb_term(term: &str) -> String {
2244 normalize_ws(term.split('|').next().unwrap_or(term))
2245}
2246
2247fn cursor(sections: &[NodeId], containers: &[NodeId], root: NodeId) -> NodeId {
2248 containers
2249 .last()
2250 .or(sections.last())
2251 .copied()
2252 .unwrap_or(root)
2253}
2254
2255impl TextModel {
2256 pub fn apply_cast(&mut self, rows: &[CastRow]) {
2270 for row in rows {
2271 let known = self
2272 .nodes
2273 .iter()
2274 .any(|n| n.kind == Kind::Character && n.onym.as_deref() == Some(row.id.as_str()));
2275 if known {
2276 continue;
2277 }
2278 let root = NodeId(0);
2279 let id = push(&mut self.nodes, Kind::Character, root);
2280 let n = &mut self.nodes[id.0 as usize];
2281 n.lemma = Some(row.name.clone()).filter(|l| !l.is_empty());
2282 n.onym = Some(row.id.clone());
2283 n.fields = row.fields.clone();
2284 }
2285 let patterns: Vec<(&CastRow, ®ex::Regex, usize)> = rows
2289 .iter()
2290 .filter_map(|r| r.pattern.as_ref().map(|p| (r, p, r.words.max(1))))
2291 .collect();
2292 let widest = patterns.iter().map(|(_, _, w)| *w).max().unwrap_or(0);
2293 if widest > 0 {
2294 let words: Vec<usize> = (0..self.nodes.len())
2295 .filter(|&i| {
2296 self.nodes[i]
2297 .token
2298 .as_ref()
2299 .is_some_and(|t| t.class == TokenClass::Word)
2300 })
2301 .collect();
2302 for (k, &i) in words.iter().enumerate() {
2303 if self.nodes[i].fields.iter().any(|(k, _)| k == "prosopon") {
2304 continue;
2305 }
2306 let mut run = vec![self.nodes[i].text.as_str()];
2307 for &j in words.iter().skip(k + 1).take(widest - 1) {
2308 if self.nodes[j].parent != self.nodes[i].parent {
2309 break;
2310 }
2311 run.push(self.nodes[j].text.as_str());
2312 }
2313 let hit = patterns.iter().find(|(_, p, w)| {
2317 (1..=(*w).min(run.len()))
2318 .rev()
2319 .any(|n| p.is_match(&run[..n].join(" ")))
2320 });
2321 if let Some((row, _, _)) = hit {
2322 self.nodes[i]
2323 .fields
2324 .push(("prosopon".to_string(), row.id.clone()));
2325 }
2326 }
2327 }
2328 let chains: Vec<(&CastRow, &str)> = rows
2332 .iter()
2333 .filter_map(|r| {
2334 r.fields
2335 .iter()
2336 .find(|(k, _)| k == "cluster" || k == "chain")
2337 .map(|(_, v)| (r, v.as_str()))
2338 })
2339 .collect();
2340 if !chains.is_empty() {
2341 let firsts: Vec<(NodeId, String)> = self
2342 .mentions
2343 .iter()
2344 .filter_map(|m| {
2345 let c = m.cluster.as_deref()?;
2346 let (row, _) = chains.iter().find(|(_, id)| *id == c)?;
2347 Some((*m.tokens.first()?, row.id.clone()))
2348 })
2349 .collect();
2350 for (t, id) in firsts {
2351 let f = &mut self.nodes[t.0 as usize].fields;
2352 if !f.iter().any(|(k, _)| k == "prosopon") {
2353 f.push(("prosopon".to_string(), id));
2354 }
2355 }
2356 }
2357 if !patterns.is_empty() {
2361 for i in 0..self.nodes.len() {
2362 let n = &self.nodes[i];
2363 if n.kind != Kind::Dialogue || n.fields.iter().any(|(k, _)| k == "prosopon") {
2364 continue;
2365 }
2366 let Some(label) = n.lemma.clone() else {
2367 continue;
2368 };
2369 if let Some((row, _, _)) = patterns.iter().find(|(_, p, _)| p.is_match(&label)) {
2370 self.nodes[i]
2371 .fields
2372 .push(("prosopon".to_string(), row.id.clone()));
2373 }
2374 }
2375 }
2376 self.link_characters();
2377 }
2378
2379 fn link_characters(&mut self) {
2383 let mut by_onym: std::collections::HashMap<String, NodeId> =
2384 std::collections::HashMap::new();
2385 for (i, n) in self.nodes.iter().enumerate() {
2386 if n.kind == Kind::Character
2387 && let Some(o) = &n.onym
2388 {
2389 by_onym.entry(o.clone()).or_insert(NodeId(i as u64));
2390 }
2391 }
2392 for n in &mut self.nodes {
2393 n.mentions.clear();
2394 }
2395 for i in 0..self.nodes.len() {
2396 if self.nodes[i].kind == Kind::Character {
2397 continue;
2398 }
2399 let target = self.nodes[i]
2400 .fields
2401 .iter()
2402 .find(|(k, _)| k == "prosopon")
2403 .and_then(|(_, v)| by_onym.get(v).copied());
2404 self.nodes[i].character = target;
2405 if let Some(c) = target {
2406 self.nodes[c.0 as usize].mentions.push(NodeId(i as u64));
2407 }
2408 }
2409 }
2410
2411 pub fn apply_places(&mut self, rows: &[CastRow]) {
2420 for row in rows {
2421 let known = self
2422 .nodes
2423 .iter()
2424 .any(|n| n.kind == Kind::Place && n.onym.as_deref() == Some(row.id.as_str()));
2425 if known {
2426 continue;
2427 }
2428 let root = NodeId(0);
2429 let id = push(&mut self.nodes, Kind::Place, root);
2430 let n = &mut self.nodes[id.0 as usize];
2431 n.lemma = Some(row.name.clone()).filter(|l| !l.is_empty());
2432 n.onym = Some(row.id.clone());
2433 n.fields = row.fields.clone();
2434 }
2435 let patterns: Vec<(&CastRow, ®ex::Regex, usize)> = rows
2436 .iter()
2437 .filter_map(|r| r.pattern.as_ref().map(|p| (r, p, r.words.max(1))))
2438 .collect();
2439 let widest = patterns.iter().map(|(_, _, w)| *w).max().unwrap_or(0);
2440 if widest > 0 {
2441 let words: Vec<usize> = (0..self.nodes.len())
2442 .filter(|&i| {
2443 self.nodes[i]
2444 .token
2445 .as_ref()
2446 .is_some_and(|t| t.class == TokenClass::Word)
2447 })
2448 .collect();
2449 for (k, &i) in words.iter().enumerate() {
2450 if self.nodes[i].fields.iter().any(|(k, _)| k == "chora") {
2451 continue;
2452 }
2453 let mut run = vec![self.nodes[i].text.as_str()];
2454 for &j in words.iter().skip(k + 1).take(widest - 1) {
2455 if self.nodes[j].parent != self.nodes[i].parent {
2456 break;
2457 }
2458 run.push(self.nodes[j].text.as_str());
2459 }
2460 let hit = patterns.iter().find(|(_, p, w)| {
2464 (1..=(*w).min(run.len()))
2465 .rev()
2466 .any(|n| p.is_match(&run[..n].join(" ")))
2467 });
2468 if let Some((row, _, _)) = hit {
2469 self.nodes[i]
2470 .fields
2471 .push(("chora".to_string(), row.id.clone()));
2472 }
2473 }
2474 }
2475 self.link_places();
2476 }
2477
2478 fn link_places(&mut self) {
2482 let mut by_onym: std::collections::HashMap<String, NodeId> =
2483 std::collections::HashMap::new();
2484 for (i, n) in self.nodes.iter().enumerate() {
2485 if n.kind == Kind::Place
2486 && let Some(o) = &n.onym
2487 {
2488 by_onym.entry(o.clone()).or_insert(NodeId(i as u64));
2489 }
2490 }
2491 for n in &mut self.nodes {
2492 if n.kind == Kind::Place {
2493 n.mentions.clear();
2494 }
2495 }
2496 for i in 0..self.nodes.len() {
2497 if self.nodes[i].kind == Kind::Place {
2498 continue;
2499 }
2500 let target = self.nodes[i]
2501 .fields
2502 .iter()
2503 .find(|(k, _)| k == "chora")
2504 .and_then(|(_, v)| by_onym.get(v).copied());
2505 self.nodes[i].place = target;
2506 if let Some(c) = target {
2507 self.nodes[c.0 as usize].mentions.push(NodeId(i as u64));
2508 }
2509 }
2510 }
2511
2512 pub fn tokenize(&mut self) {
2513 self.tokenize_with(None)
2514 }
2515
2516 fn prose_blocks(&self) -> Vec<NodeId> {
2519 (0..self.nodes.len())
2520 .filter(|&id| {
2521 let node = &self.nodes[id];
2522 let prose_block = match node.kind {
2523 Kind::Paragraph => !node.children.iter().any(|c| {
2528 let c = &self.nodes[c.0 as usize];
2529 c.kind == Kind::Sentence && !c.derived
2530 }),
2531 Kind::Sentence => !node.derived,
2532 Kind::UnorderedItem | Kind::OrderedItem | Kind::Stichos => true,
2533 Kind::Speech | Kind::Dialogue => true,
2534 Kind::Footnote | Kind::Endnote | Kind::Aside => !node.deixis,
2535 _ => false,
2536 };
2537 prose_block && !node.prose.is_empty() && !self.in_apparatus(NodeId(id as u64))
2538 })
2539 .map(|id| NodeId(id as u64))
2540 .collect()
2541 }
2542
2543 fn note_body_of(&self, node: NodeId) -> Option<NodeId> {
2548 let mut cur = self.nodes[node.0 as usize].parent;
2549 while let Some(p) = cur {
2550 let n = &self.nodes[p.0 as usize];
2551 if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis {
2552 return Some(p);
2553 }
2554 cur = n.parent;
2555 }
2556 None
2557 }
2558
2559 fn in_apparatus(&self, node: NodeId) -> bool {
2560 let n = &self.nodes[node.0 as usize];
2561 (matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis)
2562 || self.note_body_of(node).is_some()
2563 }
2564
2565 pub fn corpus_text(&self) -> String {
2569 self.prose_blocks()
2570 .iter()
2571 .map(|&b| self.nodes[b.0 as usize].prose.as_str())
2572 .collect::<Vec<_>>()
2573 .join("\n\n")
2574 }
2575
2576 pub fn is_tokenized(&self) -> bool {
2578 self.nodes.iter().any(|n| n.kind == Kind::Token)
2579 }
2580
2581 pub fn tokenize_with(&mut self, bonds: Option<&syndesmos::Syndesmos>) {
2585 use unicode_segmentation::UnicodeSegmentation;
2586 self.token_index = Default::default();
2587 let mut n: u32 = 0;
2588 let mut sentence: u32 = 0;
2589 for block in self.prose_blocks() {
2590 let id = block.0 as usize;
2591 let prose = self.nodes[id].prose.clone();
2592 let declared_sentences = std::mem::take(&mut self.nodes[id].declared_sentences);
2599 let mut declared_tokens = std::mem::take(&mut self.nodes[id].declared_tokens);
2600 declared_tokens.sort_by_key(|t| t.lo);
2601 let mut spans = if declared_sentences.is_empty() {
2602 sentence_spans(&prose, bonds)
2603 } else {
2604 declared_sentences
2605 .iter()
2606 .map(|(lo, hi, _)| (*lo as usize, *hi as usize))
2607 .filter(|(lo, hi)| lo < hi && *hi <= prose.len())
2608 .collect()
2609 };
2610 if spans.is_empty() {
2611 spans.push((0, prose.len()));
2612 }
2613 let sentence_ids: Vec<Option<String>> = if declared_sentences.is_empty() {
2614 Vec::new()
2615 } else {
2616 declared_sentences
2617 .iter()
2618 .map(|(_, _, sid)| Some(sid.clone()))
2619 .collect()
2620 };
2621 let sentences: Vec<(usize, usize, u32, NodeId)> = spans
2622 .into_iter()
2623 .enumerate()
2624 .map(|(k, (lo, hi))| {
2625 sentence += 1;
2626 let node = self.push_derived_sentence(block, &prose[lo..hi], sentence);
2627 if let Some(Some(sid)) = sentence_ids.get(k) {
2628 self.nodes[node.0 as usize]
2629 .fields
2630 .push(("sent_id".to_string(), sid.clone()));
2631 }
2632 (lo, hi, sentence, node)
2633 })
2634 .collect();
2635 let mut by_onym: Vec<(String, NodeId)> = Vec::new();
2637 let mut heads: Vec<(NodeId, String)> = Vec::new();
2638 let mut next_declared = 0usize;
2639 let mut skip_until = 0usize;
2640 for (at, seg) in prose.split_word_bound_indices() {
2641 if at < skip_until {
2642 continue;
2643 }
2644 while next_declared < declared_tokens.len()
2648 && (declared_tokens[next_declared].lo as usize) < at
2649 {
2650 next_declared += 1;
2651 }
2652 if let Some(d) = declared_tokens.get(next_declared)
2653 && d.lo as usize == at
2654 && (d.hi as usize) > at
2655 && (d.hi as usize) <= prose.len()
2656 {
2657 let d = d.clone();
2658 next_declared += 1;
2659 skip_until = d.hi as usize;
2660 let form = &prose[d.lo as usize..d.hi as usize];
2661 n += 1;
2662 let (in_sentence, span, parent) = sentences
2663 .iter()
2664 .find(|(lo, hi, _, _)| *lo <= at && at < *hi)
2665 .or_else(|| sentences.last())
2666 .map(|(lo, hi, s, node)| (*s, (*lo as u32, *hi as u32), *node))
2667 .expect("a block has at least one sentence");
2668 let tok = self.push_parsed(parent, form, &d, n, in_sentence);
2669 if let Some(t) = self.nodes[tok.0 as usize].token.as_mut() {
2670 t.span = span;
2671 t.at = at as u32;
2672 }
2673 if let Some(o) = &d.onym {
2674 by_onym.push((o.clone(), tok));
2675 }
2676 if let Some(h) = d.parsings.first().and_then(|p| p.kephale.clone()) {
2677 heads.push((tok, h));
2678 }
2679 continue;
2680 }
2681 if seg.chars().all(char::is_whitespace) {
2682 continue;
2683 }
2684 let class = class_of(seg);
2685 n += 1;
2686 let (in_sentence, span, parent) = sentences
2687 .iter()
2688 .find(|(lo, hi, _, _)| *lo <= at && at < *hi)
2689 .or_else(|| sentences.last())
2690 .map(|(lo, hi, s, node)| (*s, (*lo as u32, *hi as u32), *node))
2691 .expect("a block has at least one sentence");
2692 let tok = push(&mut self.nodes, Kind::Token, parent);
2693 let t = &mut self.nodes[tok.0 as usize];
2694 t.text = seg.to_string();
2695 t.prose = seg.to_string();
2696 t.token = Some(TokenInfo {
2697 class,
2698 n,
2699 sentence: in_sentence,
2700 span,
2701 annot: None,
2702 at: at as u32,
2703 spans: Vec::new(),
2704 });
2705 }
2706 for (tok, h) in heads {
2708 let Some((_, head)) = by_onym.iter().find(|(o, _)| *o == h) else {
2709 continue;
2710 };
2711 let head = *head;
2712 if let Some(a) = self.nodes[tok.0 as usize]
2713 .token
2714 .as_mut()
2715 .and_then(|t| t.annot.as_mut())
2716 {
2717 a.head = Some(head);
2718 }
2719 if let Some(a) = self.nodes[head.0 as usize]
2720 .token
2721 .as_mut()
2722 .and_then(|t| t.annot.as_mut())
2723 {
2724 a.dependents.push(tok);
2725 }
2726 }
2727 }
2728 self.attribute_spans();
2729 }
2730
2731 fn push_parsed(
2739 &mut self,
2740 sentence_node: NodeId,
2741 form: &str,
2742 d: &DeclaredToken,
2743 n: u32,
2744 sentence: u32,
2745 ) -> NodeId {
2746 let node = push(&mut self.nodes, Kind::Token, sentence_node);
2747 let t = &mut self.nodes[node.0 as usize];
2748 t.text = form.to_string();
2749 t.prose = form.to_string();
2750 let first = d.parsings.first().cloned().unwrap_or_default();
2751 let (upos, xpos) = match first.meros.as_deref() {
2752 Some(m) => match m.split_once('/') {
2753 Some((u, x)) => (Some(u.to_string()), Some(x.to_string())),
2754 None => (Some(m.to_string()), None),
2755 },
2756 None => (None, None),
2757 };
2758 let feats = first.parepomena.clone();
2759 let mut keys = match feats.as_deref() {
2763 Some(f) if f.split([',', '|']).all(|p| p.contains('=')) => {
2764 key_values(&f.replace(',', "|"))
2765 }
2766 _ => Vec::new(),
2767 };
2768 if let Some(sem) = &first.semasia {
2769 keys.push(("sem".to_string(), sem.clone()));
2770 }
2771 for alt in d.parsings.iter().skip(1) {
2772 let mut v = alt.lexema.clone().unwrap_or_default();
2773 if let Some(m) = &alt.meros {
2774 v.push(' ');
2775 v.push_str(m);
2776 }
2777 if let Some(f) = &alt.parepomena {
2778 v.push(' ');
2779 v.push_str(f);
2780 }
2781 keys.push(("alt".to_string(), v.trim().to_string()));
2782 }
2783 t.token = Some(TokenInfo {
2784 class: class_of_annotated(form, upos.as_deref().unwrap_or("")),
2785 n,
2786 sentence,
2787 span: (0, 0),
2788 at: 0,
2789 spans: Vec::new(),
2790 annot: Some(Box::new(Annot {
2791 id: d.onym.clone().unwrap_or_else(|| n.to_string()),
2792 lemma: first.lexema.clone(),
2793 upos,
2794 xpos,
2795 feats,
2796 deprel: first.schesis.clone(),
2797 deps: None,
2798 misc: None,
2799 keys,
2800 mwt: None,
2801 empty: false,
2802 head: None,
2803 dependents: Vec::new(),
2804 eheads: Vec::new(),
2805 edependents: Vec::new(),
2806 mentions: Vec::new(),
2807 })),
2808 });
2809 node
2810 }
2811
2812 fn attribute_spans(&mut self) {
2816 let spans: Vec<NodeId> = (0..self.nodes.len())
2817 .filter(|&i| self.nodes[i].kind == Kind::Span && self.nodes[i].at.is_some())
2818 .map(|i| NodeId(i as u64))
2819 .collect();
2820 if spans.is_empty() {
2821 return;
2822 }
2823 for span in spans {
2824 let (lo, hi) = self.nodes[span.0 as usize].at.expect("filtered");
2825 let mut block = span;
2828 while self.nodes[block.0 as usize].kind == Kind::Span
2829 && let Some(p) = self.nodes[block.0 as usize].parent
2830 {
2831 block = p;
2832 }
2833 let mut tokens = Vec::new();
2834 self.collect_tokens(block, &mut tokens);
2835 let covered: Vec<NodeId> = tokens
2836 .into_iter()
2837 .filter(|t| {
2838 let at = self.nodes[t.0 as usize].token.as_ref().map(|t| t.at);
2839 at.is_some_and(|at| lo <= at && at < hi)
2840 })
2841 .collect();
2842 for &t in &covered {
2843 if let Some(info) = self.nodes[t.0 as usize].token.as_mut()
2844 && !info.spans.contains(&span)
2845 {
2846 info.spans.push(span);
2847 }
2848 }
2849 self.nodes[span.0 as usize].span_tokens = covered;
2850 }
2851 for n in self.nodes.iter_mut() {
2854 if let Some(t) = n.token.as_mut() {
2855 t.spans.sort();
2856 }
2857 }
2858 }
2859
2860 fn collect_tokens(&self, node: NodeId, out: &mut Vec<NodeId>) {
2861 for &c in &self.nodes[node.0 as usize].children {
2862 let n = &self.nodes[c.0 as usize];
2863 if n.token.is_some() {
2864 out.push(c);
2865 } else if n.kind == Kind::Sentence {
2866 self.collect_tokens(c, out);
2867 }
2868 }
2869 }
2870
2871 fn push_derived_sentence(&mut self, block: NodeId, text: &str, ordinal: u32) -> NodeId {
2878 let node = push(&mut self.nodes, Kind::Sentence, block);
2879 let s = &mut self.nodes[node.0 as usize];
2880 s.text = text.trim().to_string();
2881 s.prose = s.text.clone();
2882 s.taxis = Some(ordinal as i64);
2883 s.derived = true;
2884 node
2885 }
2886
2887 pub fn annotate_conllu(&mut self, conllu: &str) -> Result<(), String> {
2901 if self.is_tokenized() {
2902 return Err("the document is already tokenized".into());
2903 }
2904 self.token_index = Default::default();
2905 let blocks = self.prose_blocks();
2906 let mut doc = String::new();
2908 let mut ranges: Vec<(usize, usize, NodeId)> = Vec::new();
2909 for (i, &b) in blocks.iter().enumerate() {
2910 if i > 0 {
2911 doc.push_str("\n\n");
2912 }
2913 let lo = doc.len();
2914 doc.push_str(&self.nodes[b.0 as usize].prose);
2915 ranges.push((lo, doc.len(), b));
2916 }
2917 if ranges.is_empty() {
2918 return Err("the document has no prose to annotate".into());
2919 }
2920 let parsed = parse_conllu(conllu)?;
2921 let mut cursor = 0usize;
2922 let mut block_at = 0usize;
2923 let mut n: u32 = 0;
2924 for (si, sent) in parsed.sentences.iter().enumerate() {
2925 let ordinal = si as u32 + 1;
2926 let mut made: Vec<(NodeId, &ConlluToken)> = Vec::new();
2927 let mut extents: Vec<(NodeId, usize, usize)> = Vec::new();
2932 let mut nodes_by_block: Vec<(NodeId, NodeId)> = Vec::new();
2933 let mut range: Option<(usize, usize, u32, String, String)> = None;
2936 for tok in &sent.tokens {
2937 if !tok.empty && tok.form.trim().is_empty() {
2940 continue;
2941 }
2942 let (lo, hi, mwt) = if tok.empty {
2943 (cursor, cursor, None)
2944 } else if let Some((lo, hi, until, id, form)) = &range
2945 && tok.range.is_none()
2946 && tok.ord <= *until
2947 {
2948 (*lo, *hi, Some((id.clone(), form.clone())))
2949 } else {
2950 range = None;
2951 while doc[cursor..].starts_with(char::is_whitespace) {
2952 cursor += doc[cursor..].chars().next().map_or(0, char::len_utf8);
2953 }
2954 if !doc[cursor..].starts_with(tok.form.as_str()) {
2955 let seen: String = doc[cursor..].chars().take(24).collect();
2956 return Err(format!(
2957 "CoNLL-U sentence {ordinal}, token {}: {:?} does not align with the text at offset {cursor} ({seen:?})",
2958 tok.id, tok.form
2959 ));
2960 }
2961 let lo = cursor;
2962 cursor += tok.form.len();
2963 if let Some((_, b)) = tok.range {
2964 range = Some((lo, cursor, b, tok.id.clone(), tok.form.clone()));
2965 continue;
2966 }
2967 (lo, cursor, None)
2968 };
2969 if !tok.empty {
2970 while block_at + 1 < ranges.len() && lo >= ranges[block_at].1 {
2971 block_at += 1;
2972 }
2973 }
2974 let (blo, _, block) = ranges[block_at];
2975 if !tok.empty {
2976 n += 1;
2977 }
2978 let sentence_node = match nodes_by_block.iter().find(|(b, _)| *b == block) {
2979 Some((_, s)) => *s,
2980 None => {
2981 let s = self.push_derived_sentence(block, "", ordinal);
2982 nodes_by_block.push((block, s));
2983 s
2984 }
2985 };
2986 let node = self.push_annotated(
2987 sentence_node,
2988 tok,
2989 if tok.empty { 0 } else { n },
2990 ordinal,
2991 mwt,
2992 );
2993 if !tok.empty {
2994 match extents.iter_mut().find(|(b, _, _)| *b == block) {
2995 Some(e) => {
2996 e.1 = e.1.min(lo - blo);
2997 e.2 = e.2.max(hi - blo);
2998 }
2999 None => extents.push((block, lo - blo, hi - blo)),
3000 }
3001 if let Some(t) = self.nodes[node.0 as usize].token.as_mut() {
3002 t.at = (lo - blo) as u32;
3003 }
3004 }
3005 made.push((node, tok));
3006 }
3007 for (block, s) in &nodes_by_block {
3010 if let Some((_, lo, hi)) = extents.iter().find(|(b, _, _)| b == block) {
3011 let text = self.nodes[block.0 as usize].prose[*lo..*hi]
3012 .trim()
3013 .to_string();
3014 let sn = &mut self.nodes[s.0 as usize];
3015 sn.text = text.clone();
3016 sn.prose = text;
3017 }
3018 }
3019 for (node, _) in &made {
3020 let sentence_node = self.nodes[node.0 as usize]
3021 .parent
3022 .expect("token has a sentence");
3023 let block = self.nodes[sentence_node.0 as usize]
3024 .parent
3025 .expect("sentence has a block");
3026 if let Some((_, lo, hi)) = extents.iter().find(|(b, _, _)| *b == block)
3027 && let Some(t) = self.nodes[node.0 as usize].token.as_mut()
3028 {
3029 t.span = (*lo as u32, *hi as u32);
3030 }
3031 }
3032 self.link_sentence(&made);
3033 }
3034 let header = parsed
3035 .globals
3036 .iter()
3037 .find(|(k, _)| k == "Entity")
3038 .map(|(_, v)| v.as_str());
3039 self.decode_mentions(header);
3040 Ok(())
3041 }
3042
3043 pub fn parse_conllu_text(text: &str) -> Result<Self, String> {
3050 Self::from_conllu_set(&[(None, text)], false)
3051 }
3052
3053 pub fn parse_conllu_corpus(text: &str) -> Result<Self, String> {
3060 Self::from_conllu_set(&[(None, text)], true)
3061 }
3062
3063 pub fn parse_conllu_text_set(files: &[(&str, &str)]) -> Result<Self, String> {
3070 let named: Vec<(Option<&str>, &str)> = files.iter().map(|(n, t)| (Some(*n), *t)).collect();
3071 Self::from_conllu_set(&named, false)
3072 }
3073
3074 pub fn parse_conllu_corpus_set(files: &[(&str, &str)]) -> Result<Self, String> {
3079 let named: Vec<(Option<&str>, &str)> = files.iter().map(|(n, t)| (Some(*n), *t)).collect();
3080 Self::from_conllu_set(&named, true)
3081 }
3082
3083 fn from_conllu_set(files: &[(Option<&str>, &str)], tokens: bool) -> Result<Self, String> {
3084 let mut parsed: Vec<(Option<&str>, ConlluDoc)> = Vec::with_capacity(files.len());
3085 for (name, text) in files {
3086 let doc = parse_conllu(text).map_err(|e| match name {
3087 Some(n) => format!("{n}: {e}"),
3088 None => e,
3089 })?;
3090 parsed.push((*name, doc));
3091 }
3092 let mut nodes = vec![Node::new(Kind::Document, None)];
3093 let root = NodeId(0);
3094 let mut sentences: Vec<(NodeId, usize, usize)> = Vec::new();
3096 for (fi, (name, doc)) in parsed.iter().enumerate() {
3097 let (base, doc_level) = match name {
3098 Some(n) => {
3099 let sec = push(&mut nodes, Kind::Section, root);
3100 nodes[sec.0 as usize].lemma = Some(n.to_string());
3101 nodes[sec.0 as usize].level = Some(1);
3102 (sec, 2)
3103 }
3104 None => (root, 1),
3105 };
3106 let mut section: Option<NodeId> = None;
3107 let mut para: Option<NodeId> = None;
3108 for (si, s) in doc.sentences.iter().enumerate() {
3109 if let Some(id) = &s.newdoc {
3110 let sec = push(&mut nodes, Kind::Section, base);
3111 nodes[sec.0 as usize].lemma = id.clone();
3112 nodes[sec.0 as usize].level = Some(doc_level);
3113 section = Some(sec);
3114 para = None;
3115 }
3116 if s.newpar.is_some() {
3117 let p = push(&mut nodes, Kind::Paragraph, section.unwrap_or(base));
3118 para = Some(p);
3119 }
3120 let parent = para.or(section).unwrap_or(base);
3121 let sent = push(&mut nodes, Kind::Sentence, parent);
3122 nodes[sent.0 as usize].taxis = Some(sentences.len() as i64 + 1);
3123 let text = s
3124 .comments
3125 .iter()
3126 .find(|(k, _)| k == "text")
3127 .map(|(_, v)| v.clone())
3128 .unwrap_or_else(|| conllu_surface(&s.tokens));
3129 nodes[sent.0 as usize].text = text;
3130 nodes[sent.0 as usize].fields = s.comments.clone();
3131 sentences.push((sent, fi, si));
3132 }
3133 }
3134 flatten_prose(&mut nodes);
3135 let mut model = TextModel {
3136 nodes,
3137 root,
3138 onyms: Default::default(),
3139 document_url: None,
3140 link_base: None,
3141 document_path: None,
3142 cite_scheme: None,
3143 orthography: None,
3144 bib_aliases: Default::default(),
3145 head: HeadMeta::default(),
3146 token_index: Default::default(),
3147 mentions: Vec::new(),
3148 };
3149 if tokens {
3150 let mut n: u32 = 0;
3151 for (i, (sent, fi, si)) in sentences.into_iter().enumerate() {
3152 let s = &parsed[fi].1.sentences[si];
3153 let ordinal = i as u32 + 1;
3154 let len = model.nodes[sent.0 as usize].prose.len() as u32;
3155 let mut made: Vec<(NodeId, &ConlluToken)> = Vec::new();
3156 let mut range: Option<(u32, String, String)> = None;
3158 for tok in &s.tokens {
3159 if let Some((_, b)) = tok.range {
3160 range = Some((b, tok.id.clone(), tok.form.clone()));
3161 continue;
3162 }
3163 let mwt = match &range {
3164 Some((until, id, form)) if !tok.empty && tok.ord <= *until => {
3165 Some((id.clone(), form.clone()))
3166 }
3167 _ => {
3168 range = None;
3169 None
3170 }
3171 };
3172 if !tok.empty {
3173 n += 1;
3174 }
3175 let node = model.push_annotated(
3176 sent,
3177 tok,
3178 if tok.empty { 0 } else { n },
3179 ordinal,
3180 mwt,
3181 );
3182 if let Some(t) = model.nodes[node.0 as usize].token.as_mut() {
3183 t.span = (0, len);
3184 }
3185 made.push((node, tok));
3186 }
3187 model.link_sentence(&made);
3188 }
3189 let header = parsed
3192 .iter()
3193 .flat_map(|(_, d)| d.globals.iter())
3194 .find(|(k, _)| k == "Entity")
3195 .map(|(_, v)| v.as_str());
3196 model.decode_mentions(header);
3197 }
3198 Ok(model)
3199 }
3200
3201 fn push_annotated(
3203 &mut self,
3204 block: NodeId,
3205 tok: &ConlluToken,
3206 n: u32,
3207 sentence: u32,
3208 mwt: Option<(String, String)>,
3209 ) -> NodeId {
3210 let node = push(&mut self.nodes, Kind::Token, block);
3211 let t = &mut self.nodes[node.0 as usize];
3212 t.text = tok.form.clone();
3213 t.prose = tok.form.clone();
3214 let opt = |v: &str| (v != "_" && !v.is_empty()).then(|| v.to_string());
3215 let mut keys = key_values(&tok.feats);
3216 keys.extend(key_values(&tok.misc));
3217 keys.extend(tok.extra.iter().cloned());
3218 t.token = Some(TokenInfo {
3219 class: class_of_annotated(&tok.form, &tok.upos),
3220 n,
3221 sentence,
3222 span: (0, 0),
3223 at: 0,
3224 spans: Vec::new(),
3225 annot: Some(Box::new(Annot {
3226 id: tok.id.clone(),
3227 lemma: opt(&tok.lemma),
3228 upos: opt(&tok.upos),
3229 xpos: opt(&tok.xpos),
3230 feats: opt(&tok.feats),
3231 deprel: opt(&tok.deprel),
3232 deps: opt(&tok.deps),
3233 misc: opt(&tok.misc),
3234 keys,
3235 mwt,
3236 empty: tok.empty,
3237 head: None,
3238 dependents: Vec::new(),
3239 eheads: Vec::new(),
3240 edependents: Vec::new(),
3241 mentions: Vec::new(),
3242 })),
3243 });
3244 node
3245 }
3246
3247 fn link_sentence(&mut self, made: &[(NodeId, &ConlluToken)]) {
3251 let find = |id: &str| made.iter().find(|(_, t)| t.id == id).map(|(n, _)| *n);
3252 for (node, tok) in made {
3253 if !tok.empty
3254 && tok.head != "0"
3255 && tok.head != "_"
3256 && !tok.head.is_empty()
3257 && let Some(h) = find(&tok.head)
3258 {
3259 if let Some(a) = self.nodes[node.0 as usize]
3260 .token
3261 .as_mut()
3262 .and_then(|t| t.annot.as_mut())
3263 {
3264 a.head = Some(h);
3265 }
3266 if let Some(a) = self.nodes[h.0 as usize]
3267 .token
3268 .as_mut()
3269 .and_then(|t| t.annot.as_mut())
3270 {
3271 a.dependents.push(*node);
3272 }
3273 }
3274 if tok.deps == "_" || tok.deps.is_empty() {
3275 continue;
3276 }
3277 for entry in tok.deps.split('|') {
3278 let Some((h, rel)) = entry.split_once(':') else {
3279 continue;
3280 };
3281 if h == "0" {
3282 continue;
3283 }
3284 let Some(target) = find(h) else {
3285 continue;
3286 };
3287 if let Some(a) = self.nodes[node.0 as usize]
3288 .token
3289 .as_mut()
3290 .and_then(|t| t.annot.as_mut())
3291 {
3292 a.eheads.push((target, rel.to_string()));
3293 }
3294 if let Some(a) = self.nodes[target.0 as usize]
3295 .token
3296 .as_mut()
3297 .and_then(|t| t.annot.as_mut())
3298 {
3299 a.edependents.push((*node, rel.to_string()));
3300 }
3301 }
3302 }
3303 }
3304 fn decode_mentions(&mut self, entity_header: Option<&str>) {
3314 self.mentions.clear();
3315 let tokens: Vec<NodeId> = (0..self.nodes.len())
3316 .filter(|&i| {
3317 self.nodes[i]
3318 .token
3319 .as_ref()
3320 .is_some_and(|t| t.annot.is_some())
3321 })
3322 .map(|i| NodeId(i as u64))
3323 .collect();
3324 let key = |nodes: &[Node], t: NodeId, name: &str| -> Option<String> {
3325 nodes[t.0 as usize]
3326 .token
3327 .as_ref()?
3328 .annot
3329 .as_ref()?
3330 .key(name)
3331 .map(str::to_string)
3332 };
3333 let sentence_of = |nodes: &[Node], t: NodeId| -> u32 {
3334 nodes[t.0 as usize].token.as_ref().map_or(0, |t| t.sentence)
3335 };
3336 let mut open: Option<(usize, Option<String>, u32)> = None;
3338 for &t in &tokens {
3339 let Some(tag) = key(&self.nodes, t, "ner") else {
3340 open = None;
3341 continue;
3342 };
3343 let (prefix, etype) = match tag.split_once('-') {
3344 Some((p, e)) => (p.to_string(), Some(e.to_string())),
3345 None => (tag.clone(), None),
3346 };
3347 let sentence = sentence_of(&self.nodes, t);
3348 let continues = matches!(prefix.as_str(), "I" | "E")
3349 && open
3350 .as_ref()
3351 .is_some_and(|(_, ty, s)| *ty == etype && *s == sentence);
3352 match prefix.as_str() {
3353 "B" | "S" | "I" | "E" => {
3354 if continues {
3355 let (idx, _, _) = open.as_ref().unwrap();
3356 self.mentions[*idx].tokens.push(t);
3357 } else {
3358 self.mentions.push(Mention {
3359 tokens: vec![t],
3360 etype: etype.clone(),
3361 cluster: None,
3362 });
3363 }
3364 let idx = if continues {
3365 open.as_ref().unwrap().0
3366 } else {
3367 self.mentions.len() - 1
3368 };
3369 open = if matches!(prefix.as_str(), "B" | "I") {
3370 Some((idx, etype, sentence))
3371 } else {
3372 None
3373 };
3374 }
3375 _ => open = None,
3376 }
3377 }
3378 let order: Vec<String> = entity_header
3380 .unwrap_or("eid-etype-head-other")
3381 .split('-')
3382 .map(str::to_string)
3383 .collect();
3384 let mut stack: Vec<(String, usize)> = Vec::new();
3385 for &t in &tokens {
3386 let Some(spec) = key(&self.nodes, t, "Entity") else {
3387 for (_, idx) in &stack {
3388 if self.mentions[*idx].tokens.last() != Some(&t) {
3389 self.mentions[*idx].tokens.push(t);
3390 }
3391 }
3392 continue;
3393 };
3394 let mut closings: Vec<String> = Vec::new();
3395 for entry in entity_entries(&spec) {
3396 let entry = entry.as_str();
3397 let opening = entry.starts_with('(');
3398 let closing = entry.ends_with(')');
3399 let body = entry.trim_start_matches('(').trim_end_matches(')');
3400 if opening {
3401 let mut attrs = body.splitn(order.len(), '-');
3402 let eid = attrs.next().unwrap_or("").to_string();
3403 let mut etype = None;
3404 for (name, val) in order.iter().skip(1).zip(attrs) {
3405 if name == "etype" && !val.is_empty() {
3406 etype = Some(val.to_string());
3407 }
3408 }
3409 let cluster = eid.split('[').next().unwrap_or(&eid).to_string();
3412 let idx = self.mentions.len();
3413 self.mentions.push(Mention {
3414 tokens: vec![t],
3415 etype,
3416 cluster: Some(cluster),
3417 });
3418 if !closing {
3419 stack.push((eid, idx));
3420 }
3421 } else if closing {
3422 closings.push(body.to_string());
3423 }
3424 }
3425 for (_, idx) in &stack {
3426 if self.mentions[*idx].tokens.last() != Some(&t) {
3427 self.mentions[*idx].tokens.push(t);
3428 }
3429 }
3430 for eid in closings {
3431 if let Some(pos) = stack.iter().rposition(|(e, _)| *e == eid) {
3432 stack.remove(pos);
3433 }
3434 }
3435 }
3436 for (i, m) in self.mentions.iter().enumerate() {
3438 for &t in &m.tokens {
3439 if let Some(a) = self.nodes[t.0 as usize]
3440 .token
3441 .as_mut()
3442 .and_then(|t| t.annot.as_mut())
3443 {
3444 a.mentions.push(i as u32 + 1);
3445 }
3446 }
3447 }
3448 }
3449
3450 fn mention_text(&self, m: &Mention) -> String {
3453 let mut out = String::new();
3454 for (i, &t) in m.tokens.iter().enumerate() {
3455 let n = &self.nodes[t.0 as usize];
3456 if i > 0 {
3457 let prev = &self.nodes[m.tokens[i - 1].0 as usize];
3458 let glued = prev
3459 .token
3460 .as_ref()
3461 .and_then(|t| t.annot.as_ref())
3462 .and_then(|a| a.key("SpaceAfter"))
3463 == Some("No");
3464 if !glued {
3465 out.push(' ');
3466 }
3467 }
3468 out.push_str(&n.prose);
3469 }
3470 out
3471 }
3472
3473 fn token_field(&self, node: NodeId, name: &str) -> Option<Value> {
3476 let t = self.nodes[node.0 as usize].token.as_ref()?;
3477 if let Some(v) = field_values(&self.nodes[node.0 as usize].fields, name) {
3479 return Some(v);
3480 }
3481 for &s in t.spans.iter().rev() {
3482 if let Some(v) = field_values(&self.nodes[s.0 as usize].fields, name) {
3483 return Some(v);
3484 }
3485 }
3486 let mut at = self.nodes[node.0 as usize].parent;
3487 while let Some(p) = at {
3488 let n = &self.nodes[p.0 as usize];
3489 if !(n.kind == Kind::Sentence && n.derived) {
3490 return field_values(&n.fields, name);
3491 }
3492 at = n.parent;
3493 }
3494 None
3495 }
3496
3497 fn mention_values(&self, node: NodeId, f: impl Fn(&Mention) -> Option<Value>) -> Option<Value> {
3499 let a = self.nodes[node.0 as usize].token.as_ref()?.annot.as_ref()?;
3500 let vs: Vec<Value> = a
3501 .mentions
3502 .iter()
3503 .filter_map(|&i| f(&self.mentions[i as usize - 1]))
3504 .collect();
3505 match vs.len() {
3506 0 => None,
3507 1 => vs.into_iter().next(),
3508 _ => Some(Value::list(vs)),
3509 }
3510 }
3511}
3512
3513fn enclosing_span(nodes: &[Node], flow: NodeId, lo: u32, hi: u32) -> Option<NodeId> {
3518 let mut best: Option<NodeId> = None;
3519 let mut at = flow;
3520 loop {
3521 let next = nodes[at.0 as usize].children.iter().rev().find_map(|&c| {
3522 let n = &nodes[c.0 as usize];
3523 match (n.kind, n.at) {
3524 (Kind::Span, Some((slo, shi)))
3525 if slo <= lo && hi <= shi && (slo, shi) != (lo, hi) =>
3526 {
3527 Some(c)
3528 }
3529 _ => None,
3530 }
3531 });
3532 match next {
3533 Some(c) => {
3534 best = Some(c);
3535 at = c;
3536 }
3537 None => return best,
3538 }
3539 }
3540}
3541
3542fn field_values(fields: &[(String, String)], name: &str) -> Option<Value> {
3545 let vs: Vec<Value> = fields
3546 .iter()
3547 .filter(|(k, _)| k == name)
3548 .map(|(_, v)| Value::Str(v.clone()))
3549 .collect();
3550 match vs.len() {
3551 0 => None,
3552 1 => vs.into_iter().next(),
3553 _ => Some(Value::list(vs)),
3554 }
3555}
3556
3557fn push(nodes: &mut Vec<Node>, kind: Kind, parent: NodeId) -> NodeId {
3558 let id = NodeId(nodes.len() as u64);
3559 nodes.push(Node::new(kind, Some(parent)));
3560 nodes[parent.0 as usize].children.push(id);
3561 id
3562}
3563
3564fn lower_table(
3570 nodes: &mut Vec<Node>,
3571 parent: NodeId,
3572 lemma: Option<String>,
3573 headers: Option<Vec<String>>,
3574 rows: Vec<Vec<Cell>>,
3575) {
3576 let list = push(nodes, Kind::OrderedList, parent);
3577 {
3578 let n = &mut nodes[list.0 as usize];
3579 n.table = true;
3580 n.lemma = lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
3581 }
3582 for (i, row) in rows.into_iter().enumerate() {
3583 let item = push(nodes, Kind::OrderedItem, list);
3584 nodes[item.0 as usize].taxis = Some(i as i64 + 1);
3585 nodes[item.0 as usize].row = true;
3586 let cells = push(nodes, Kind::UnorderedList, item);
3587 for (j, cell) in row.into_iter().enumerate() {
3588 let value = normalize_ws(&cell.text);
3589 if value.is_empty() {
3590 continue;
3591 }
3592 let label = cell
3593 .label
3594 .as_deref()
3595 .or_else(|| headers.as_ref().and_then(|h| h.get(j)).map(|h| h.as_str()))
3596 .map(normalize_ws)
3597 .filter(|h| !h.is_empty());
3598 let cell_item = push(nodes, Kind::UnorderedItem, cells);
3599 let n = &mut nodes[cell_item.0 as usize];
3600 n.cell = true;
3601 n.lemma = label;
3602 n.text = value;
3603 }
3604 }
3605}
3606
3607fn lower_verse(
3613 nodes: &mut Vec<Node>,
3614 parent: NodeId,
3615 lemma: Option<String>,
3616 strophes: Vec<Vec<String>>,
3617 hypograph: Option<String>,
3618) -> NodeId {
3619 let verse = push(nodes, Kind::Verse, parent);
3620 {
3621 let n = &mut nodes[verse.0 as usize];
3622 n.lemma = lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
3623 n.hypograph = hypograph
3624 .map(|h| normalize_ws(&h))
3625 .filter(|h| !h.is_empty());
3626 }
3627 let mut line_no = 0i64;
3628 for (i, strophe) in strophes.into_iter().enumerate() {
3629 let sid = push(nodes, Kind::Strophe, verse);
3630 nodes[sid.0 as usize].taxis = Some(i as i64 + 1);
3631 for line in strophe {
3632 let line = line.trim_end().to_string();
3633 if line.trim().is_empty() {
3634 continue;
3635 }
3636 line_no += 1;
3637 let lid = push(nodes, Kind::Stichos, sid);
3638 let n = &mut nodes[lid.0 as usize];
3639 n.taxis = Some(line_no);
3640 n.text = line;
3641 }
3642 }
3643 verse
3644}
3645
3646fn flatten_prose(nodes: &mut [Node]) {
3656 for i in (0..nodes.len()).rev() {
3657 let inline_lemma = matches!(nodes[i].kind, Kind::UnorderedItem | Kind::OrderedItem);
3658 let mut lemma_part: Option<String> = None;
3659 let mut parts: Vec<String> = Vec::new();
3660 if let Some(lemma) = &nodes[i].lemma
3661 && !lemma.is_empty()
3662 {
3663 if inline_lemma {
3664 lemma_part = Some(lemma.clone());
3665 } else if matches!(nodes[i].kind, Kind::Paragraph | Kind::Dialogue) {
3666 } else {
3670 parts.push(lemma.clone());
3671 }
3672 }
3673 if !nodes[i].text.is_empty() {
3674 parts.push(nodes[i].text.clone());
3675 }
3676 let mut child_parts: Vec<String> = Vec::new();
3677 for &child in nodes[i].children.clone().iter() {
3678 if matches!(
3682 nodes[child.0 as usize].kind,
3683 Kind::Ref | Kind::Cit | Kind::Anchor | Kind::Span | Kind::Milestone
3684 ) {
3685 continue;
3686 }
3687 let prose = &nodes[child.0 as usize].prose;
3688 if !prose.is_empty() {
3689 child_parts.push(prose.clone());
3690 }
3691 }
3692 if nodes[i].kind == Kind::Verse {
3696 let joined = child_parts.join("\n\n");
3697 if !joined.is_empty() {
3698 parts.push(joined);
3699 }
3700 } else if nodes[i].kind == Kind::Paragraph
3701 && !child_parts.is_empty()
3702 && nodes[i]
3703 .children
3704 .iter()
3705 .all(|c| nodes[c.0 as usize].kind == Kind::Sentence)
3706 {
3707 parts.push(child_parts.join(" "));
3710 } else {
3711 parts.extend(child_parts);
3712 }
3713 if let Some(hypograph) = &nodes[i].hypograph
3714 && !hypograph.is_empty()
3715 {
3716 parts.push(hypograph.clone());
3717 }
3718 let mut prose = parts.join("\n");
3719 if let Some(lemma) = lemma_part {
3720 prose = if prose.is_empty() {
3721 lemma
3722 } else {
3723 format!("{lemma}: {prose}")
3724 };
3725 }
3726 nodes[i].prose = prose;
3727 }
3728}
3729
3730impl TextModel {
3731 fn grammata(&self, node: NodeId) -> String {
3737 let n = &self.nodes[node.0 as usize];
3738 let mut s = n.prose.as_str();
3739 if let Some(lemma) = &n.lemma
3740 && !lemma.is_empty()
3741 && let Some(rest) = s.strip_prefix(lemma.as_str())
3742 {
3743 s = rest
3744 .strip_prefix(": ")
3745 .or_else(|| rest.strip_prefix('\n'))
3746 .unwrap_or(rest);
3747 }
3748 if let Some(h) = &n.hypograph
3749 && !h.is_empty()
3750 && let Some(rest) = s.strip_suffix(h.as_str())
3751 {
3752 s = rest.strip_suffix('\n').unwrap_or(rest);
3753 }
3754 s.trim_end().to_string()
3755 }
3756}
3757
3758impl AstAdapter for TextModel {
3759 fn root(&self) -> NodeId {
3760 self.root
3761 }
3762
3763 fn children(&self, node: NodeId) -> Vec<NodeId> {
3764 self.nodes[node.0 as usize].children.clone()
3765 }
3766
3767 fn name(&self, node: NodeId) -> Option<String> {
3768 let n = &self.nodes[node.0 as usize];
3769 if n.kind == Kind::Span {
3770 return n.span_kind.clone();
3771 }
3772 n.kind.name().map(str::to_string)
3773 }
3774
3775 fn parent(&self, node: NodeId) -> Option<NodeId> {
3776 self.nodes[node.0 as usize].parent
3777 }
3778
3779 fn descendants_where(
3785 &self,
3786 node: NodeId,
3787 name: &str,
3788 property: &str,
3789 value: &Value,
3790 ) -> Option<Vec<(NodeId, usize)>> {
3791 if name != "token" || property != "lower" {
3792 return None;
3793 }
3794 let Value::Str(word) = value else {
3795 return None;
3796 };
3797 if word.is_empty() || !word.chars().all(|c| c.is_alphabetic() || "’'-".contains(c)) {
3798 return None;
3799 }
3800 let index = self.token_index.get_or_init(|| {
3801 let mut map: std::collections::HashMap<String, Vec<NodeId>> =
3802 std::collections::HashMap::new();
3803 for (i, n) in self.nodes.iter().enumerate() {
3804 if n.token.is_some() {
3805 map.entry(self.lower(&n.prose))
3806 .or_default()
3807 .push(NodeId(i as u64));
3808 }
3809 }
3810 map
3811 });
3812 let Some(hits) = index.get(word.as_str()) else {
3813 return Some(Vec::new());
3814 };
3815 let mut out = Vec::new();
3816 for &t in hits {
3817 let mut depth = 0usize;
3819 let mut cur = t;
3820 while let Some(p) = self.nodes[cur.0 as usize].parent {
3821 depth += 1;
3822 if p == node {
3823 out.push((t, depth));
3824 break;
3825 }
3826 cur = p;
3827 }
3828 }
3829 Some(out)
3830 }
3831
3832 fn traits(&self, node: NodeId) -> Vec<String> {
3836 let n = &self.nodes[node.0 as usize];
3837 let mut out = Vec::new();
3838 if n.kind == Kind::Document {
3846 for (ns, t) in self
3847 .head
3848 .tags
3849 .iter()
3850 .map(|t| ("tag", t))
3851 .chain(self.head.category.iter().map(|c| ("category", c)))
3852 {
3853 let name = HeadMeta::trait_name(t);
3854 if name.is_empty() {
3855 continue;
3856 }
3857 let name = format!("{ns}:{name}");
3858 if !out.contains(&name) {
3859 out.push(name);
3860 }
3861 }
3862 }
3863 if let Some(t) = &n.token {
3868 if t.annot.as_ref().is_some_and(|a| a.empty) {
3871 out.push("empty".to_string());
3872 } else {
3873 out.push(t.class.name().to_string());
3874 if t.class == TokenClass::Punct && is_quote(&n.prose) {
3877 out.push("quote".to_string());
3878 }
3879 }
3880 }
3881 if n.kind != Kind::Document
3882 && !n.deixis
3883 && !n.derived
3884 && !matches!(
3885 n.kind,
3886 Kind::IndexMark
3887 | Kind::Ref
3888 | Kind::Cit
3889 | Kind::Anchor
3890 | Kind::Strophe
3891 | Kind::Stichos
3892 | Kind::Token
3893 | Kind::Span
3894 | Kind::Milestone
3895 | Kind::Character
3896 | Kind::Place
3897 )
3898 {
3899 out.push("block".to_string());
3900 }
3901 for g in &n.genoses {
3904 if !out.contains(g) {
3905 out.push(g.clone());
3906 }
3907 }
3908 if n.derived {
3912 out.push("derived".to_string());
3913 }
3914 if matches!(n.kind, Kind::Ref | Kind::Cit) && n.dangling {
3920 out.push("dangling".to_string());
3921 }
3922 if n.kind == Kind::Bib
3925 && let Some(g) = &n.genus
3926 {
3927 out.push(g.clone());
3928 }
3929 if n.kind == Kind::Anchor
3930 || (n.onym.is_some()
3931 && !n.deixis
3932 && !matches!(
3933 n.kind,
3934 Kind::Footnote
3935 | Kind::Endnote
3936 | Kind::Aside
3937 | Kind::IndexMark
3938 | Kind::Ref
3939 | Kind::Cit
3940 | Kind::Bib
3941 | Kind::Span
3942 | Kind::Milestone
3943 ))
3944 {
3945 out.push("target".to_string());
3946 }
3947 if n.kind == Kind::Cit && n.locator.is_some() {
3948 out.push("locator".to_string());
3949 }
3950 if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) {
3951 if n.deixis {
3958 out.push("deixis".to_string());
3959 } else if n.kind != Kind::Aside {
3960 out.push("note".to_string());
3961 }
3962 if n.dangling {
3963 out.push("dangling".to_string());
3964 }
3965 }
3966 if n.table {
3967 out.push("table".to_string());
3968 }
3969 if n.row {
3970 out.push("row".to_string());
3971 }
3972 if n.cell {
3973 out.push("cell".to_string());
3974 }
3975 out
3976 }
3977
3978 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
3989 let n = &self.nodes[node.0 as usize];
3990 if name == "cite" {
3992 return self.cite(node).map(Value::Str);
3993 }
3994 if n.kind == Kind::Document {
3997 let h = &self.head;
3998 match name {
3999 "title" | "lemma" => return h.title.clone().map(Value::Str),
4000 "description" => return h.description.clone().map(Value::Str),
4001 "author" => return h.author.clone().map(Value::Str),
4002 "category" => return h.category.clone().map(Value::Str),
4003 "tags" => {
4004 return (!h.tags.is_empty())
4005 .then(|| Value::list(h.tags.iter().cloned().map(Value::Str).collect()));
4006 }
4007 "published" => return h.published.clone(),
4008 "modified" => return h.modified.clone(),
4009 _ => {}
4010 }
4011 }
4012 if n.kind == Kind::Bib && !n.fields.is_empty() {
4018 let canon = self
4019 .bib_aliases
4020 .get(&name.to_lowercase())
4021 .map(String::as_str)
4022 .unwrap_or(name);
4023 if let Some((_, v)) = n.fields.iter().find(|(k, _)| k == canon) {
4024 return Some(Value::Str(v.clone()));
4025 }
4026 }
4027 match name {
4028 "href" => return self.href(node).map(Value::Str),
4032 "anchor" => return self.nearest_onym(node).map(|(a, _)| Value::Str(a)),
4033 "link" => {
4034 let href = self.href(node)?;
4035 let text = match self.default_value(node) {
4036 Some(Value::Str(t)) => t,
4037 _ => String::new(),
4038 };
4039 return Some(Value::Record(vec![
4040 (
4041 "title".to_string(),
4042 self.document_title().map(Value::Str).unwrap_or(Value::Null),
4043 ),
4044 ("href".to_string(), Value::Str(href)),
4045 ("text".to_string(), Value::Str(text)),
4046 ]));
4047 }
4048 _ => {}
4049 }
4050 match name {
4051 "lemma" | "upos" | "xpos" | "feats" | "deprel" | "deps" | "misc" | "id" | "mwt"
4054 if n.token.as_ref().is_some_and(|t| t.annot.is_some()) =>
4055 {
4056 let a = n.token.as_ref()?.annot.as_ref()?;
4057 match name {
4058 "lemma" => a.lemma.clone(),
4059 "upos" => a.upos.clone(),
4060 "xpos" => a.xpos.clone(),
4061 "feats" => a.feats.clone(),
4062 "deprel" => a.deprel.clone(),
4063 "deps" => a.deps.clone(),
4064 "misc" => a.misc.clone(),
4065 "id" => Some(a.id.clone()),
4066 _ => a.mwt.as_ref().map(|(_, form)| form.clone()),
4067 }
4068 .map(Value::Str)
4069 }
4070 "id" if n.kind == Kind::Sentence => n
4073 .fields
4074 .iter()
4075 .find(|(k, _)| k == "sent_id")
4076 .map(|(_, v)| Value::Str(v.clone())),
4077 "lemma" | "title" => n.lemma.clone().map(Value::Str),
4078 "onym" => n.onym.clone().map(Value::Str),
4079 "term" if n.kind == Kind::IndexMark => n.onym.clone().map(Value::Str),
4081 "target" if matches!(n.kind, Kind::Ref | Kind::Cit) => n.target.clone().map(Value::Str),
4084 "locator" if n.kind == Kind::Cit => n.locator.clone().map(Value::Str),
4087 _ if n.kind == Kind::Bib => {
4091 let canon = self
4092 .bib_aliases
4093 .get(&name.to_lowercase())
4094 .map(String::as_str)
4095 .unwrap_or(name);
4096 n.fields
4097 .iter()
4098 .find(|(k, _)| k == canon)
4099 .map(|(_, v)| Value::Str(v.clone()))
4100 }
4101 "hypograph" | "attribution" => n.hypograph.clone().map(Value::Str),
4102 "taxis" | "ord" => n.taxis.map(Value::Int),
4103 "grammata" | "body" => {
4104 let g = self.grammata(node);
4105 if g.is_empty() {
4106 None
4107 } else {
4108 Some(Value::Str(g))
4109 }
4110 }
4111 "text" => Some(Value::Str(n.prose.clone())),
4112 "lower" if n.token.is_some() => Some(Value::Str(self.lower(&n.prose))),
4113 "modern" if n.token.is_some() => Some(Value::Str(self.modern(&n.prose))),
4114 "class" if n.token.is_some() => n
4115 .token
4116 .as_ref()
4117 .map(|t| Value::Str(t.class.name().to_string())),
4118 "sentence" if n.token.is_some() => {
4121 let parent = &self.nodes[n.parent?.0 as usize];
4122 if parent.kind == Kind::Sentence {
4123 return Some(Value::Str(parent.prose.clone()));
4124 }
4125 let t = n.token.as_ref()?;
4126 let (lo, hi) = (t.span.0 as usize, t.span.1 as usize);
4127 parent
4128 .prose
4129 .get(lo..hi)
4130 .map(|s| Value::Str(s.trim().to_string()))
4131 }
4132 "mention" if n.token.is_some() => {
4135 self.mention_values(node, |m| Some(Value::Str(self.mention_text(m))))
4136 }
4137 "entity" if n.token.is_some() => {
4138 self.mention_values(node, |m| m.etype.clone().map(Value::Str))
4139 }
4140 _ if n.token.as_ref().is_some_and(|t| t.annot.is_some()) => n
4146 .token
4147 .as_ref()?
4148 .annot
4149 .as_ref()?
4150 .key(name)
4151 .map(|v| Value::Str(v.to_string()))
4152 .or_else(|| self.token_field(node, name)),
4153 _ if n.kind == Kind::Sentence => n
4156 .fields
4157 .iter()
4158 .find(|(k, _)| k == name)
4159 .or_else(|| n.fields.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)))
4160 .map(|(_, v)| Value::Str(v.clone())),
4161 _ if matches!(
4166 n.kind,
4167 Kind::Span
4168 | Kind::Speech
4169 | Kind::Dialogue
4170 | Kind::Paragraph
4171 | Kind::Milestone
4172 | Kind::Character
4173 | Kind::Place
4174 ) && !n.fields.is_empty() =>
4175 {
4176 field_values(&n.fields, name)
4177 }
4178 _ if n.token.is_some() => self.token_field(node, name),
4182 _ => None,
4183 }
4184 }
4185
4186 fn default_value(&self, node: NodeId) -> Option<Value> {
4189 let n = &self.nodes[node.0 as usize];
4190 if n.kind == Kind::IndexMark {
4191 return Some(Value::Str(n.onym.clone().unwrap_or_default()));
4194 }
4195 if n.deixis {
4196 return Some(Value::Str(match n.note_edge {
4200 Some(body) => self.nodes[body.0 as usize].prose.clone(),
4201 None => n.onym.clone().unwrap_or_default(),
4202 }));
4203 }
4204 if n.kind == Kind::Ref {
4205 if !n.text.is_empty() {
4210 return Some(Value::Str(n.text.clone()));
4211 }
4212 return Some(Value::Str(match n.ref_edge {
4213 Some(t) => {
4214 let b = &self.nodes[t.0 as usize];
4215 b.lemma.clone().unwrap_or_else(|| b.prose.clone())
4216 }
4217 None => n.target.clone().unwrap_or_default(),
4218 }));
4219 }
4220 if n.kind == Kind::Bib && n.prose.is_empty() && !n.fields.is_empty() {
4221 return Some(Value::Str(
4224 n.fields
4225 .iter()
4226 .map(|(_, v)| v.as_str())
4227 .collect::<Vec<_>>()
4228 .join(". "),
4229 ));
4230 }
4231 if n.kind == Kind::Cit {
4232 return Some(Value::Str(n.target.clone().unwrap_or_default()));
4236 }
4237 if n.kind == Kind::Anchor {
4238 return Some(Value::Str(n.onym.clone().unwrap_or_default()));
4241 }
4242 Some(Value::Str(n.prose.clone()))
4243 }
4244
4245 fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
4249 let n = &self.nodes[node.0 as usize];
4250 let character: Vec<(String, NodeId)> = n
4252 .character
4253 .map(|c| ("character".to_string(), c))
4254 .into_iter()
4255 .chain(n.place.map(|p| ("place".to_string(), p)))
4256 .collect();
4257 if let Some(a) = n.token.as_ref().and_then(|t| t.annot.as_ref()) {
4261 let mut out = Vec::new();
4262 if let Some(h) = a.head {
4263 out.push(("head".to_string(), h));
4264 }
4265 for (t, _) in &a.eheads {
4266 out.push(("ehead".to_string(), *t));
4267 }
4268 out.extend(character);
4269 return out;
4270 }
4271 if !character.is_empty() && n.kind != Kind::Span {
4272 return character;
4273 }
4274 if let Some(t) = n.ref_edge {
4275 let label = if n.kind == Kind::Cit { "cit" } else { "ref" };
4278 return vec![(label.to_string(), t)];
4279 }
4280 if n.kind == Kind::Span && !(n.span_tokens.is_empty() && character.is_empty()) {
4283 return n
4284 .span_tokens
4285 .iter()
4286 .map(|&t| ("token".to_string(), t))
4287 .chain(character)
4288 .collect();
4289 }
4290 match n.note_edge {
4291 Some(body) => vec![(n.kind.name().unwrap_or("footnote").to_string(), body)],
4292 None => Vec::new(),
4293 }
4294 }
4295
4296 fn link_property(
4300 &self,
4301 source: NodeId,
4302 label: &str,
4303 target: NodeId,
4304 name: &str,
4305 ) -> Option<Value> {
4306 if name != "rel" {
4307 return None;
4308 }
4309 let a = self.nodes[source.0 as usize]
4310 .token
4311 .as_ref()?
4312 .annot
4313 .as_ref()?;
4314 match label {
4315 "head" if a.head == Some(target) => a.deprel.clone().map(Value::Str),
4316 "ehead" => a
4317 .eheads
4318 .iter()
4319 .find(|(t, _)| *t == target)
4320 .map(|(_, r)| Value::Str(r.clone())),
4321 _ => None,
4322 }
4323 }
4324
4325 fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
4326 let n = &self.nodes[node.0 as usize];
4327 if let Some(a) = n.token.as_ref().and_then(|t| t.annot.as_ref()) {
4328 return a
4329 .dependents
4330 .iter()
4331 .map(|&d| ("head".to_string(), d))
4332 .chain(a.edependents.iter().map(|(d, _)| ("ehead".to_string(), *d)))
4333 .collect();
4334 }
4335 let mut out: Vec<(String, NodeId)> = n
4336 .ref_cites
4337 .iter()
4338 .map(|&c| {
4339 let label = if self.nodes[c.0 as usize].kind == Kind::Cit {
4340 "cit"
4341 } else {
4342 "ref"
4343 };
4344 (label.to_string(), c)
4345 })
4346 .collect();
4347 let label = n.kind.name().unwrap_or("footnote");
4348 out.extend(n.cites.iter().map(|&c| (label.to_string(), c)));
4349 let mention_label = if n.kind == Kind::Place {
4350 "place"
4351 } else {
4352 "character"
4353 };
4354 out.extend(n.mentions.iter().map(|&m| (mention_label.to_string(), m)));
4355 out
4356 }
4357
4358 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
4364 let n = &self.nodes[node.0 as usize];
4365 if !matches!(n.kind, Kind::Ref | Kind::Cit) || property != "target" {
4366 return None;
4367 }
4368 let landed = n.ref_edge?;
4369 let is_point = self.nodes[landed.0 as usize].kind == Kind::Anchor;
4370 match hint {
4371 None | Some("*") => Some(landed),
4372 Some("point") => is_point.then_some(landed),
4373 Some("block") => {
4374 if !is_point {
4375 return Some(landed);
4376 }
4377 let mut at = self.nodes[landed.0 as usize].parent;
4379 while let Some(b) = at {
4380 let bn = &self.nodes[b.0 as usize];
4381 if bn.kind != Kind::Document
4382 && !matches!(bn.kind, Kind::Ref | Kind::Anchor | Kind::IndexMark)
4383 {
4384 return Some(b);
4385 }
4386 at = bn.parent;
4387 }
4388 None
4389 }
4390 Some(_) => None,
4391 }
4392 }
4393
4394 fn ref_property(&self, node: NodeId) -> Option<String> {
4396 matches!(self.nodes[node.0 as usize].kind, Kind::Ref | Kind::Cit)
4397 .then(|| "target".to_string())
4398 }
4399
4400 fn external_ref(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<String> {
4404 if !matches!(hint, None | Some("*")) {
4405 return None;
4406 }
4407 let n = &self.nodes[node.0 as usize];
4408 if n.kind != Kind::Ref || property != "target" || n.internal {
4409 return None;
4410 }
4411 let t = n.target.as_deref()?.trim();
4412 if t.is_empty() || t.starts_with('#') {
4413 return None;
4414 }
4415 let u = match url::Url::parse(t) {
4416 Ok(u) => u,
4417 Err(url::ParseError::RelativeUrlWithoutBase) => self
4418 .link_base
4419 .as_ref()
4420 .or(self.document_url.as_ref())?
4421 .join(t)
4422 .ok()?,
4423 Err(_) => return None,
4424 };
4425 if u.scheme() != "http" && u.scheme() != "https" {
4426 return None;
4427 }
4428 Some(u.into())
4429 }
4430
4431 fn resolve_fragment(&self, _node: NodeId, fragment: &str) -> Option<NodeId> {
4434 self.onyms.get(fragment).copied()
4435 }
4436
4437 fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
4441 &["level", "lang", "form", "n"]
4442 }
4443
4444 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
4447 let n = &self.nodes[node.0 as usize];
4448 if n.kind == Kind::Document
4451 && let Some(v) = self.head.declared_value(key)
4452 {
4453 return Some(v);
4454 }
4455 match key {
4456 "level" => n.level.map(|l| Value::Int(l as i64)),
4457 "lang" => n
4461 .lang
4462 .clone()
4463 .or_else(|| {
4464 let i = n.genoses.iter().position(|g| g == "foreign")?;
4465 n.genoses.get(i + 1).cloned()
4466 })
4467 .map(Value::Str),
4468 "at" if n.at.is_some() => n.at.map(|(lo, _)| Value::Int(lo as i64)),
4469 "n" | "sentence" if n.kind == Kind::Sentence => n.taxis.map(Value::Int),
4474 "n" => n
4475 .token
4476 .as_ref()
4477 .filter(|t| t.n > 0)
4478 .map(|t| Value::Int(t.n as i64)),
4479 "sentence" => n.token.as_ref().map(|t| Value::Int(t.sentence as i64)),
4480 "mention" if n.token.is_some() => {
4483 let ords: Vec<Value> = n
4484 .token
4485 .as_ref()
4486 .and_then(|t| t.annot.as_ref())
4487 .map(|a| a.mentions.iter().map(|&i| Value::Int(i as i64)).collect())
4488 .unwrap_or_default();
4489 match ords.len() {
4490 0 => None,
4491 1 => ords.into_iter().next(),
4492 _ => Some(Value::list(ords)),
4493 }
4494 }
4495 "entity" if n.token.is_some() => {
4496 self.mention_values(node, |m| m.cluster.clone().map(Value::Str))
4497 }
4498 "mwt" => n
4501 .token
4502 .as_ref()
4503 .and_then(|t| t.annot.as_ref())
4504 .and_then(|a| a.mwt.as_ref())
4505 .map(|(id, _)| Value::Str(id.clone())),
4506 "resolved" if n.deixis => Some(Value::Bool(!n.dangling)),
4509 "resolved" if n.kind == Kind::Cit || (n.kind == Kind::Ref && n.internal) => {
4513 Some(Value::Bool(n.ref_edge.is_some()))
4514 }
4515 "form" if n.margin => Some(Value::Str("margin".to_string())),
4518 "genus" if n.kind == Kind::Bib => n.genus.clone().map(Value::Str),
4520 _ => None,
4521 }
4522 }
4523}