1use quarb::{AstAdapter, NodeId, Value};
53
54mod ann;
55pub mod render;
56mod tier;
57pub use render::{Render, render_node, render_nodes};
58
59#[derive(Debug, Clone, PartialEq)]
63pub enum Block {
64 Heading { level: u8, lemma: String },
67 Paragraph { text: String },
69 Dialogue {
74 lemma: String,
75 text: String,
76 lit: bool,
80 },
81 Speech {
87 text: String,
88 genoses: Vec<String>,
89 fields: Vec<(String, String)>,
90 },
91 Span {
99 kind: String,
100 lo: u32,
101 hi: u32,
102 genoses: Vec<String>,
103 fields: Vec<(String, String)>,
104 },
105 Hidden {
111 kind: String,
112 text: String,
113 genoses: Vec<String>,
114 fields: Vec<(String, String)>,
115 },
116 Milestone {
120 scheme: String,
121 value: String,
122 at: u32,
123 },
124 Annotate { fields: Vec<(String, String)> },
127 Parsed {
134 lo: u32,
135 hi: u32,
136 onym: Option<String>,
137 parsings: Vec<Parsing>,
138 },
139 Periodos { lo: u32, hi: u32, id: String },
143 Character {
148 lemma: String,
149 onym: Option<String>,
150 text: String,
151 genoses: Vec<String>,
152 },
153 Wear { genoses: Vec<String> },
158 Text { text: String },
162 Open {
165 kind: Container,
166 lemma: Option<String>,
167 },
168 Close { hypograph: Option<String> },
171 Verbatim { lang: Option<String>, text: String },
174 NoteRef {
182 onym: String,
183 family: Option<NoteFamily>,
184 margin: bool,
189 at: Option<u32>,
193 },
194 IndexMark { term: String },
198 Ref {
207 target: String,
208 text: Option<String>,
209 internal: bool,
210 },
211 Anchor { onym: String },
216 Cite {
223 target: String,
224 locator: Option<String>,
225 },
226 Bib {
234 key: String,
235 text: String,
236 fields: Vec<(String, String)>,
237 genus: Option<String>,
238 },
239 Label { onym: String },
244 Verse {
250 lemma: Option<String>,
251 strophes: Vec<Vec<String>>,
252 hypograph: Option<String>,
253 },
254 Table {
261 lemma: Option<String>,
262 headers: Option<Vec<String>>,
263 rows: Vec<Vec<Cell>>,
264 },
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct Cell {
273 pub label: Option<String>,
274 pub text: String,
275}
276
277impl From<&str> for Cell {
278 fn from(text: &str) -> Self {
279 Cell {
280 label: None,
281 text: text.to_string(),
282 }
283 }
284}
285
286impl From<String> for Cell {
287 fn from(text: String) -> Self {
288 Cell { label: None, text }
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum Container {
295 Blockquote,
296 UnorderedList,
297 OrderedList {
299 start: i64,
300 },
301 Item,
303 Note {
308 onym: String,
309 family: NoteFamily,
310 margin: bool,
311 },
312 Entry {
316 taxis: Option<i64>,
317 onym: Option<String>,
318 },
319 Sense {
322 taxis: Option<i64>,
323 },
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333pub enum NoteFamily {
334 Footnote,
335 Endnote,
336 Aside,
337}
338
339impl NoteFamily {
340 fn kind(self) -> Kind {
341 match self {
342 NoteFamily::Footnote => Kind::Footnote,
343 NoteFamily::Endnote => Kind::Endnote,
344 NoteFamily::Aside => Kind::Aside,
345 }
346 }
347}
348
349#[derive(Debug, Clone, Default, PartialEq, Eq)]
355pub struct Parsing {
356 pub lexema: Option<String>,
357 pub meros: Option<String>,
358 pub parepomena: Option<String>,
359 pub semasia: Option<String>,
360 pub kephale: Option<String>,
361 pub schesis: Option<String>,
362}
363
364#[derive(Debug, Clone)]
367struct DeclaredToken {
368 lo: u32,
369 hi: u32,
370 onym: Option<String>,
371 parsings: Vec<Parsing>,
372}
373
374#[derive(Debug, Clone)]
379pub struct CastRow {
380 pub id: String,
381 pub name: String,
382 pub pattern: Option<regex::Regex>,
383 pub words: usize,
386 pub fields: Vec<(String, String)>,
387}
388
389impl CastRow {
390 pub fn read_csv(path: &str) -> Result<Vec<CastRow>, String> {
397 let mut reader = csv::Reader::from_path(path).map_err(|e| e.to_string())?;
398 let headers: Vec<String> = reader
399 .headers()
400 .map_err(|e| e.to_string())?
401 .iter()
402 .map(str::to_string)
403 .collect();
404 if !headers.iter().any(|h| h == "id") {
405 return Err(format!(
406 "a cast table needs an id column (found: {})",
407 headers.join(", ")
408 ));
409 }
410 let mut rows = Vec::new();
411 for record in reader.records() {
412 let record = record.map_err(|e| e.to_string())?;
413 let get = |k: &str| {
414 headers
415 .iter()
416 .position(|h| h == k)
417 .and_then(|i| record.get(i))
418 .map(str::trim)
419 .filter(|v| !v.is_empty())
420 };
421 let Some(id) = get("id") else { continue };
422 let (pattern, words) = if let Some(p) = get("pattern") {
423 let re = regex::Regex::new(p).map_err(|e| format!("cast pattern for {id}: {e}"))?;
424 (Some(re), p.split_whitespace().count().max(1))
427 } else if let Some(stem) = get("stem").or_else(|| get("prefix")) {
428 let re = regex::Regex::new(&format!("^{}", regex::escape(stem))).unwrap();
429 (Some(re), stem.split_whitespace().count())
430 } else if let Some(form) = get("mention").or_else(|| get("form")) {
431 let re = regex::Regex::new(&format!("^{}$", regex::escape(form))).unwrap();
432 (Some(re), form.split_whitespace().count())
433 } else {
434 (None, 0)
435 };
436 let fields: Vec<(String, String)> = headers
437 .iter()
438 .zip(record.iter())
439 .filter(|(h, v)| h.as_str() != "id" && !v.trim().is_empty())
440 .map(|(h, v)| (h.clone(), v.trim().to_string()))
441 .collect();
442 rows.push(CastRow {
443 id: id.to_string(),
444 name: get("name").unwrap_or(id).to_string(),
445 pattern,
446 words,
447 fields,
448 });
449 }
450 Ok(rows)
451 }
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
456enum Kind {
457 Document,
458 Section,
459 Character,
463 Place,
467 Paragraph,
468 Blockquote,
469 UnorderedList,
470 OrderedList,
471 UnorderedItem,
472 OrderedItem,
473 Verbatim,
474 Footnote,
478 Endnote,
480 Aside,
484 IndexMark,
486 Ref,
488 Cit,
491 Bib,
494 Anchor,
496 Verse,
498 Strophe,
500 Stichos,
502 Sentence,
518 Speech,
521 Dialogue,
524 Span,
527 Milestone,
529 Entry,
531 Sense,
533}
534
535impl Kind {
536 fn name(self) -> Option<&'static str> {
537 Some(match self {
538 Kind::Document => return None,
539 Kind::Section => "section",
540 Kind::Paragraph => "paragraph",
541 Kind::Blockquote => "blockquote",
542 Kind::Footnote => "footnote",
543 Kind::Endnote => "endnote",
544 Kind::Aside => "aside",
545 Kind::IndexMark => "index-mark",
546 Kind::Ref => "ref",
547 Kind::Cit => "cit",
548 Kind::Bib => "bib",
549 Kind::Anchor => "anchor",
550 Kind::Verse => "verse",
551 Kind::Strophe => "strophe",
552 Kind::Stichos => "stichos",
553 Kind::UnorderedList => "unordered-list",
554 Kind::OrderedList => "ordered-list",
555 Kind::UnorderedItem => "unordered-item",
556 Kind::OrderedItem => "ordered-item",
557 Kind::Verbatim => "verbatim",
558 Kind::Sentence => "sentence",
559 Kind::Speech => "dialogue-line",
560 Kind::Character => "character",
561 Kind::Place => "place",
562 Kind::Dialogue => "dialogue",
563 Kind::Span => "span",
565 Kind::Milestone => "milestone",
566 Kind::Entry => "entry",
567 Kind::Sense => "sense",
568 })
569 }
570}
571
572struct Node {
573 kind: Kind,
574 lemma: Option<String>,
575 hypograph: Option<String>,
576 taxis: Option<i64>,
577 level: Option<u8>,
579 lang: Option<String>,
581 start: i64,
584 text: String,
586 prose: String,
588 table: bool,
590 row: bool,
592 cell: bool,
594 onym: Option<String>,
597 deixis: bool,
599 dangling: bool,
601 family_open: bool,
604 margin: bool,
606 note_edge: Option<NodeId>,
608 cites: Vec<NodeId>,
610 target: Option<String>,
612 locator: Option<String>,
614 internal: bool,
616 fields: Vec<(String, String)>,
619 genus: Option<String>,
620 ref_edge: Option<NodeId>,
622 ref_cites: Vec<NodeId>,
624 parent: Option<NodeId>,
625 children: Vec<NodeId>,
626 derived: bool,
631 genoses: Vec<String>,
635 span_kind: Option<String>,
636 at: Option<(u32, u32)>,
637 span_tokens: Vec<NodeId>,
638 character: Option<NodeId>,
641 mentions: Vec<NodeId>,
642 place: Option<NodeId>,
645 declared_tokens: Vec<DeclaredToken>,
648 declared_sentences: Vec<(u32, u32, String)>,
649}
650
651#[derive(Debug, Clone, Copy, PartialEq, Eq)]
653enum TokenClass {
654 Word,
656 Number,
658 Punct,
660}
661
662impl TokenClass {
663 fn name(self) -> &'static str {
664 match self {
665 TokenClass::Word => "word",
666 TokenClass::Number => "number",
667 TokenClass::Punct => "punct",
668 }
669 }
670}
671
672#[derive(Debug, Clone)]
678struct Mention {
679 tokens: Vec<NodeId>,
680 etype: Option<String>,
682 cluster: Option<String>,
684}
685
686fn entity_entries(spec: &str) -> Vec<String> {
691 let mut out = Vec::new();
692 for piece in spec.split('|') {
693 let mut cur = String::new();
694 for ch in piece.chars() {
695 if ch == '(' && !cur.is_empty() {
696 out.push(std::mem::take(&mut cur));
697 }
698 cur.push(ch);
699 if ch == ')' {
700 out.push(std::mem::take(&mut cur));
701 }
702 }
703 if !cur.is_empty() {
704 out.push(cur);
705 }
706 }
707 out
708}
709
710fn key_values(column: &str) -> Vec<(String, String)> {
713 if column == "_" || column.is_empty() {
714 return Vec::new();
715 }
716 column
717 .split('|')
718 .filter(|e| !e.is_empty())
719 .map(|e| match e.split_once('=') {
720 Some((k, v)) => (k.to_string(), v.to_string()),
721 None => (e.to_string(), String::new()),
722 })
723 .collect()
724}
725
726fn sentence_spans(prose: &str, bonds: Option<&syndesmos::Syndesmos>) -> Vec<(usize, usize)> {
731 let raw: Vec<(usize, &str)> = match bonds {
732 Some(b) => b.split_sentence_bound_indices(prose),
733 None => {
734 use unicode_segmentation::UnicodeSegmentation;
735 prose.split_sentence_bound_indices().collect()
736 }
737 };
738 raw.into_iter()
739 .filter(|(_, seg)| !seg.trim().is_empty())
740 .map(|(at, seg)| (at, at + seg.len()))
741 .collect()
742}
743
744const CONLLU_COLUMNS: [&str; 10] = [
746 "ID", "FORM", "LEMMA", "UPOS", "XPOS", "FEATS", "HEAD", "DEPREL", "DEPS", "MISC",
747];
748
749#[derive(Debug, Clone, Default)]
751struct ConlluToken {
752 id: String,
753 range: Option<(u32, u32)>,
755 ord: u32,
757 empty: bool,
759 form: String,
760 lemma: String,
761 upos: String,
762 xpos: String,
763 feats: String,
764 head: String,
765 deprel: String,
766 deps: String,
767 misc: String,
768 extra: Vec<(String, String)>,
771}
772
773#[derive(Debug, Clone, Default)]
775struct ConlluSentence {
776 comments: Vec<(String, String)>,
778 newdoc: Option<Option<String>>,
780 newpar: Option<Option<String>>,
782 tokens: Vec<ConlluToken>,
783}
784
785#[derive(Debug, Clone, Default)]
788struct ConlluDoc {
789 sentences: Vec<ConlluSentence>,
790 globals: Vec<(String, String)>,
791}
792
793pub fn looks_like_conllu(text: &str) -> bool {
800 let mut columns = CONLLU_COLUMNS.len();
801 for line in text.lines().take(200) {
802 let line = line.trim_end_matches('\r');
803 if line.trim().is_empty() {
804 continue;
805 }
806 if let Some(c) = line.strip_prefix('#') {
807 if let Some((k, v)) = c.split_once('=')
808 && k.trim() == "global.columns"
809 {
810 columns = v.split_whitespace().count().max(2);
811 }
812 continue;
813 }
814 let fields: Vec<&str> = line.split('\t').collect();
815 if fields.len() != columns {
816 return false;
817 }
818 let id = fields[0];
819 let (a, b) = match id.split_once(['-', '.']) {
820 Some((a, b)) => (a, Some(b)),
821 None => (id, None),
822 };
823 return a.parse::<u32>().is_ok() && b.is_none_or(|b| b.parse::<u32>().is_ok());
824 }
825 false
826}
827
828pub fn read_conllu_dir(dir: &std::path::Path) -> Result<Vec<(String, String)>, String> {
834 read_conllu_dir_with(dir, &quarb::source::SourceDecoding::default())
835}
836
837pub fn read_conllu_dir_with(
841 dir: &std::path::Path,
842 decoding: &quarb::source::SourceDecoding,
843) -> Result<Vec<(String, String)>, String> {
844 fn walk(
845 dir: &std::path::Path,
846 base: &std::path::Path,
847 decoding: &quarb::source::SourceDecoding,
848 out: &mut Vec<(String, String)>,
849 ) -> Result<(), String> {
850 let mut entries: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
851 .map_err(|e| format!("reading {}: {e}", dir.display()))?
852 .filter_map(|e| e.ok().map(|e| e.path()))
853 .filter(|p| {
854 !p.file_name()
855 .and_then(|n| n.to_str())
856 .is_some_and(|n| n.starts_with('.'))
857 })
858 .collect();
859 entries.sort();
860 for p in entries {
861 if p.is_dir() {
862 walk(&p, base, decoding, out)?;
863 } else if p.extension().and_then(|e| e.to_str()).is_some_and(|e| {
864 e.eq_ignore_ascii_case("conllu") || e.eq_ignore_ascii_case("conllup")
865 }) {
866 let bytes =
867 std::fs::read(&p).map_err(|e| format!("reading {}: {e}", p.display()))?;
868 let text = decoding
869 .decode(&p.display().to_string(), &bytes)
870 .map_err(|e| format!("reading {}: {e}", p.display()))?
871 .text;
872 let text = text
873 .strip_prefix('\u{feff}')
874 .map(str::to_owned)
875 .unwrap_or(text);
876 let name = p
877 .strip_prefix(base)
878 .unwrap_or(&p)
879 .to_string_lossy()
880 .into_owned();
881 out.push((name, text));
882 }
883 }
884 Ok(())
885 }
886 let mut out = Vec::new();
887 walk(dir, dir, decoding, &mut out)?;
888 if out.is_empty() {
889 return Err(format!("{} holds no .conllu file", dir.display()));
890 }
891 Ok(out)
892}
893
894fn parse_conllu(text: &str) -> Result<ConlluDoc, String> {
902 let mut doc = ConlluDoc::default();
903 let mut current = ConlluSentence::default();
904 let mut columns: Vec<String> = CONLLU_COLUMNS.iter().map(|c| c.to_string()).collect();
905 let trimmed = |s: &str| s.trim().to_string();
906 for (i, line) in text.lines().enumerate() {
907 let line = line.trim_end_matches('\r');
908 if line.trim().is_empty() {
909 if !current.tokens.is_empty() {
910 doc.sentences.push(std::mem::take(&mut current));
911 }
912 continue;
913 }
914 if let Some(comment) = line.strip_prefix('#') {
915 let comment = comment.trim();
916 if let Some(rest) = comment.strip_prefix("newdoc") {
917 let id = rest
918 .trim()
919 .strip_prefix("id")
920 .and_then(|r| r.trim().strip_prefix('='));
921 current.newdoc = Some(id.map(trimmed));
922 } else if let Some(rest) = comment.strip_prefix("newpar") {
923 let id = rest
924 .trim()
925 .strip_prefix("id")
926 .and_then(|r| r.trim().strip_prefix('='));
927 current.newpar = Some(id.map(trimmed));
928 } else if let Some((k, v)) = comment.split_once('=') {
929 let (k, v) = (k.trim(), v.trim());
930 if let Some(g) = k.strip_prefix("global.") {
931 if g == "columns" {
932 columns = v.split_whitespace().map(|c| c.to_string()).collect();
933 }
934 doc.globals.push((g.to_string(), v.to_string()));
935 } else {
936 current.comments.push((k.to_string(), v.to_string()));
937 }
938 }
939 continue;
940 }
941 let f: Vec<&str> = line.split('\t').collect();
942 if f.len() < 2 {
943 return Err(format!(
944 "CoNLL-U line {}: expected tab-separated columns",
945 i + 1
946 ));
947 }
948 let mut tok = ConlluToken::default();
949 for (k, name) in columns.iter().enumerate() {
950 let v = f.get(k).copied().unwrap_or("_");
951 match name.to_ascii_uppercase().as_str() {
952 "ID" => tok.id = v.to_string(),
953 "FORM" => tok.form = v.to_string(),
954 "LEMMA" => tok.lemma = v.to_string(),
955 "UPOS" => tok.upos = v.to_string(),
956 "XPOS" => tok.xpos = v.to_string(),
957 "FEATS" => tok.feats = v.to_string(),
958 "HEAD" => tok.head = v.to_string(),
959 "DEPREL" => tok.deprel = v.to_string(),
960 "DEPS" => tok.deps = v.to_string(),
961 "MISC" => tok.misc = v.to_string(),
962 _ => {
963 if v != "_" && !v.is_empty() {
964 tok.extra.push((name.replace(':', "-"), v.to_string()));
965 }
966 }
967 }
968 }
969 if tok.id.is_empty() {
970 return Err(format!("CoNLL-U line {}: no token id", i + 1));
971 }
972 if let Some((a, b)) = tok.id.split_once('-') {
973 match (a.parse::<u32>(), b.parse::<u32>()) {
974 (Ok(a), Ok(b)) if a <= b => {
975 tok.range = Some((a, b));
976 tok.ord = a;
977 }
978 _ => {
979 return Err(format!(
980 "CoNLL-U line {}: malformed multiword range {:?}",
981 i + 1,
982 tok.id
983 ));
984 }
985 }
986 } else if let Some((a, _)) = tok.id.split_once('.') {
987 tok.empty = true;
988 tok.ord = a.parse().map_err(|_| {
989 format!(
990 "CoNLL-U line {}: malformed empty-node id {:?}",
991 i + 1,
992 tok.id
993 )
994 })?;
995 } else {
996 tok.ord = tok
997 .id
998 .parse()
999 .map_err(|_| format!("CoNLL-U line {}: malformed token id {:?}", i + 1, tok.id))?;
1000 }
1001 current.tokens.push(tok);
1002 }
1003 if !current.tokens.is_empty() {
1004 doc.sentences.push(current);
1005 }
1006 Ok(doc)
1007}
1008
1009fn conllu_surface(tokens: &[ConlluToken]) -> String {
1013 let mut out = String::new();
1014 let mut covered: u32 = 0; for t in tokens {
1016 if t.empty || (t.range.is_none() && t.ord <= covered) {
1017 continue;
1018 }
1019 if let Some((_, b)) = t.range {
1020 covered = b;
1021 }
1022 out.push_str(&t.form);
1023 let no_space = key_values(&t.misc)
1024 .iter()
1025 .any(|(k, v)| k.eq_ignore_ascii_case("SpaceAfter") && v == "No");
1026 if !no_space {
1027 out.push(' ');
1028 }
1029 }
1030 out.trim_end().to_string()
1031}
1032
1033fn class_of(seg: &str) -> TokenClass {
1034 if seg.chars().any(char::is_alphabetic) {
1035 TokenClass::Word
1036 } else if seg.chars().any(char::is_numeric) {
1037 TokenClass::Number
1038 } else {
1039 TokenClass::Punct
1040 }
1041}
1042
1043fn class_of_annotated(form: &str, upos: &str) -> TokenClass {
1048 match upos {
1049 "PUNCT" => TokenClass::Punct,
1050 "NUM" => TokenClass::Number,
1051 _ => class_of(form),
1052 }
1053}
1054
1055fn is_quote(form: &str) -> bool {
1060 !form.is_empty()
1061 && form.chars().all(|c| {
1062 matches!(
1063 c,
1064 '"' | '\'' | '`'
1065 | '\u{AB}' | '\u{BB}'
1066 | '\u{2018}'..='\u{201F}'
1067 | '\u{2039}' | '\u{203A}'
1068 | '\u{300C}'..='\u{300F}'
1069 | '\u{301D}'..='\u{301F}'
1070 | '\u{FF02}' | '\u{FF07}'
1071 )
1072 })
1073}
1074
1075impl Node {
1076 fn new(kind: Kind, parent: Option<NodeId>) -> Self {
1077 Node {
1078 kind,
1079 derived: false,
1080 lemma: None,
1081 hypograph: None,
1082 taxis: None,
1083 level: None,
1084 lang: None,
1085 start: 1,
1086 text: String::new(),
1087 prose: String::new(),
1088 table: false,
1089 row: false,
1090 cell: false,
1091 onym: None,
1092 deixis: false,
1093 dangling: false,
1094 family_open: false,
1095 margin: false,
1096 note_edge: None,
1097 cites: Vec::new(),
1098 target: None,
1099 locator: None,
1100 internal: false,
1101 fields: Vec::new(),
1102 genus: None,
1103 ref_edge: None,
1104 ref_cites: Vec::new(),
1105 parent,
1106 children: Vec::new(),
1107 genoses: Vec::new(),
1108 span_kind: None,
1109 at: None,
1110 span_tokens: Vec::new(),
1111 character: None,
1112 place: None,
1113 mentions: Vec::new(),
1114 declared_tokens: Vec::new(),
1115 declared_sentences: Vec::new(),
1116 }
1117 }
1118}
1119
1120fn inline_host(
1125 nodes: &[Node],
1126 containers: &[NodeId],
1127 last_flow: Option<NodeId>,
1128) -> Option<(NodeId, bool)> {
1129 let container = containers
1130 .last()
1131 .copied()
1132 .filter(|c| matches!(nodes[c.0 as usize].kind, Kind::Entry | Kind::Sense));
1133 match (container, last_flow) {
1134 (Some(c), Some(f)) if c.0 > f.0 => Some((c, true)),
1135 (Some(c), None) => Some((c, true)),
1136 (_, Some(f)) => Some((f, false)),
1137 (None, None) => None,
1138 }
1139}
1140
1141fn resolve_refs(nodes: &mut [Node]) -> std::collections::HashMap<String, NodeId> {
1148 let mut onyms: std::collections::HashMap<String, NodeId> = std::collections::HashMap::new();
1149 let mut bibs: std::collections::HashMap<String, NodeId> = std::collections::HashMap::new();
1153 for (i, n) in nodes.iter().enumerate() {
1154 if n.deixis || matches!(n.kind, Kind::IndexMark | Kind::Ref | Kind::Cit) {
1155 continue;
1156 }
1157 if n.kind == Kind::Bib {
1158 if let Some(k) = &n.onym {
1159 bibs.entry(k.clone()).or_insert(NodeId(i as u64));
1160 }
1161 continue;
1162 }
1163 let bearer = n.kind == Kind::Anchor
1164 || (!matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
1165 && n.onym.is_some());
1166 if bearer && let Some(o) = &n.onym {
1167 onyms.entry(o.clone()).or_insert(NodeId(i as u64));
1168 }
1169 }
1170 for i in 0..nodes.len() {
1171 let (map, key) = match nodes[i].kind {
1172 Kind::Ref if nodes[i].internal => (
1173 &onyms,
1174 nodes[i]
1175 .target
1176 .as_deref()
1177 .map(|t| t.trim_start_matches('#').to_string())
1178 .unwrap_or_default(),
1179 ),
1180 Kind::Cit => (&bibs, nodes[i].target.clone().unwrap_or_default()),
1181 _ => continue,
1182 };
1183 match map.get(&key) {
1184 Some(&bearer) => {
1185 nodes[i].ref_edge = Some(bearer);
1186 let me = NodeId(i as u64);
1187 nodes[bearer.0 as usize].ref_cites.push(me);
1188 }
1189 None => nodes[i].dangling = true,
1190 }
1191 }
1192 onyms
1193}
1194
1195pub fn normalize_ws(s: &str) -> String {
1199 s.split_whitespace().collect::<Vec<_>>().join(" ")
1200}
1201
1202#[derive(Debug, Clone, Default)]
1218pub struct HeadMeta {
1219 pub declared: Vec<(String, Vec<String>)>,
1222 pub title: Option<String>,
1223 pub description: Option<String>,
1224 pub author: Option<String>,
1225 pub category: Option<String>,
1227 pub tags: Vec<String>,
1229 pub published: Option<Value>,
1230 pub modified: Option<Value>,
1231}
1232
1233impl HeadMeta {
1234 pub fn declare(&mut self, name: &str, value: &str) {
1236 let value = normalize_ws(value);
1237 if name.is_empty() || value.is_empty() {
1238 return;
1239 }
1240 match self.declared.iter_mut().find(|(k, _)| k == name) {
1241 Some((_, vs)) => vs.push(value),
1242 None => self.declared.push((name.to_string(), vec![value])),
1243 }
1244 }
1245
1246 pub fn tag(&mut self, tag: &str) {
1248 let tag = normalize_ws(tag);
1249 if tag.is_empty() || self.tags.iter().any(|t| t.eq_ignore_ascii_case(&tag)) {
1250 return;
1251 }
1252 self.tags.push(tag);
1253 }
1254
1255 pub fn declared_value(&self, name: &str) -> Option<Value> {
1258 let (_, vs) = self.declared.iter().find(|(k, _)| k == name)?;
1259 Some(match vs.as_slice() {
1260 [one] => Value::Str(one.clone()),
1261 many => Value::list(many.iter().cloned().map(Value::Str).collect()),
1262 })
1263 }
1264
1265 pub fn trait_name(s: &str) -> String {
1270 let mut out = String::new();
1271 let mut dash = false;
1272 for c in s.chars() {
1273 if c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '+') {
1274 out.push(c);
1275 dash = c == '-';
1276 } else if !dash && !out.is_empty() {
1277 out.push('-');
1278 dash = true;
1279 }
1280 }
1281 while out.ends_with('-') {
1282 out.pop();
1283 }
1284 out
1285 }
1286}
1287
1288#[derive(Default)]
1293struct Caches {
1294 cite_table: Vec<(NodeId, u32, NodeId)>,
1295 prose_set: std::collections::HashSet<NodeId>,
1296 callout_of_body: std::collections::HashMap<NodeId, NodeId>,
1297}
1298
1299pub struct TextModel {
1301 nodes: Vec<Node>,
1302 root: NodeId,
1303 caches: std::cell::OnceCell<Caches>,
1304 onyms: std::collections::HashMap<String, NodeId>,
1309 document_url: Option<url::Url>,
1311 link_base: Option<url::Url>,
1315 document_path: Option<String>,
1319 cite_scheme: Option<String>,
1323 notes_omitted: bool,
1327 orthography: Option<String>,
1332 bib_aliases: std::collections::HashMap<String, String>,
1338 head: HeadMeta,
1341 token_index: std::sync::OnceLock<std::collections::HashMap<String, Vec<NodeId>>>,
1346 mentions: Vec<Mention>,
1349 tiered: bool,
1353 bonds: Option<syndesmos::Syndesmos>,
1355 tiers: std::cell::RefCell<std::collections::HashMap<usize, std::rc::Rc<tier::Tier>>>,
1357 given: std::collections::HashMap<usize, Vec<(u32, u32)>>,
1360 ords: std::cell::OnceCell<std::collections::HashMap<usize, (u32, u32)>>,
1363 virt_fields: std::collections::HashMap<NodeId, Vec<(String, String)>>,
1365 virt_character: std::collections::HashMap<NodeId, NodeId>,
1367 virt_place: std::collections::HashMap<NodeId, NodeId>,
1368 ann: Vec<ann::AnnRow>,
1371 ann_names: quarb::names::Names,
1372 ann_of: std::collections::HashMap<usize, Vec<u32>>,
1373 ann_deps: std::cell::OnceCell<std::collections::HashMap<u32, Vec<u32>>>,
1374 ann_edeps: std::cell::OnceCell<std::collections::HashMap<u32, Vec<(u32, u32)>>>,
1375}
1376
1377impl TextModel {
1378 pub fn build(blocks: Vec<Block>) -> Self {
1385 let mut nodes = vec![Node::new(Kind::Document, None)];
1386 let root = NodeId(0);
1387 let mut sections: Vec<NodeId> = Vec::new();
1389 let mut containers: Vec<NodeId> = Vec::new();
1391 let mut last_flow: Option<NodeId> = None;
1394 let mut last_block: Option<NodeId> = None;
1399
1400 for block in blocks {
1401 match block {
1402 Block::Heading { level, lemma } => {
1403 let lemma = normalize_ws(&lemma);
1404 if !containers.is_empty() {
1405 if !lemma.is_empty() {
1408 let parent = *containers.last().unwrap();
1409 let id = push(&mut nodes, Kind::Paragraph, parent);
1410 nodes[id.0 as usize].text = lemma;
1411 }
1412 continue;
1413 }
1414 while let Some(&open) = sections.last() {
1415 if nodes[open.0 as usize].level >= Some(level) {
1416 sections.pop();
1417 } else {
1418 break;
1419 }
1420 }
1421 let parent = sections.last().copied().unwrap_or(root);
1422 let id = push(&mut nodes, Kind::Section, parent);
1423 let n = &mut nodes[id.0 as usize];
1424 n.lemma = Some(lemma);
1425 n.level = Some(level);
1426 sections.push(id);
1427 last_flow = Some(id);
1428 last_block = Some(id);
1429 }
1430 Block::Paragraph { text } => {
1431 let text = normalize_ws(&text);
1432 if text.is_empty() {
1433 continue;
1434 }
1435 let parent = cursor(§ions, &containers, root);
1436 let id = push(&mut nodes, Kind::Paragraph, parent);
1437 nodes[id.0 as usize].text = text;
1438 last_flow = Some(id);
1439 last_block = Some(id);
1440 }
1441 Block::Dialogue { lemma, text, lit } => {
1443 let text = normalize_ws(&text);
1444 if text.is_empty() {
1445 continue;
1446 }
1447 let parent = cursor(§ions, &containers, root);
1448 let kind = if lit { Kind::Dialogue } else { Kind::Paragraph };
1449 let id = push(&mut nodes, kind, parent);
1450 let n = &mut nodes[id.0 as usize];
1451 n.text = text;
1452 n.lemma = Some(normalize_ws(&lemma)).filter(|l| !l.is_empty());
1453 last_flow = Some(id);
1454 last_block = Some(id);
1455 }
1456 Block::Speech {
1458 text,
1459 genoses,
1460 fields,
1461 } => {
1462 let text = normalize_ws(&text);
1463 if text.is_empty() {
1464 continue;
1465 }
1466 let parent = cursor(§ions, &containers, root);
1467 let id = push(&mut nodes, Kind::Speech, parent);
1468 let n = &mut nodes[id.0 as usize];
1469 n.text = text;
1470 n.genoses = genoses;
1471 n.fields = fields;
1472 last_flow = Some(id);
1473 last_block = Some(id);
1474 }
1475 Block::Span {
1478 kind,
1479 lo,
1480 hi,
1481 genoses,
1482 fields,
1483 } => {
1484 let Some((flow, in_lemma)) = inline_host(&nodes, &containers, last_flow) else {
1490 continue;
1491 };
1492 let host = &nodes[flow.0 as usize];
1493 let source = if in_lemma {
1494 host.lemma.as_deref().unwrap_or("")
1495 } else {
1496 host.text.as_str()
1497 };
1498 let text = source
1499 .get(lo as usize..hi as usize)
1500 .unwrap_or("")
1501 .to_string();
1502 let parent = enclosing_span(&nodes, flow, lo, hi).unwrap_or(flow);
1503 let id = push(&mut nodes, Kind::Span, parent);
1504 let n = &mut nodes[id.0 as usize];
1505 n.span_kind = Some(kind);
1506 n.at = Some((lo, hi));
1507 n.text = text;
1508 n.genoses = genoses;
1509 n.fields = fields;
1510 }
1511 Block::Hidden {
1512 kind,
1513 text,
1514 mut genoses,
1515 fields,
1516 } => {
1517 let Some((flow, in_lemma)) = inline_host(&nodes, &containers, last_flow) else {
1518 continue;
1519 };
1520 let host = &nodes[flow.0 as usize];
1521 let end = if in_lemma {
1522 host.lemma.as_deref().map_or(0, str::len) as u32
1523 } else {
1524 host.text.len() as u32
1525 };
1526 let id = push(&mut nodes, Kind::Span, flow);
1527 if !genoses.iter().any(|g| g == "hidden") {
1528 genoses.push("hidden".to_string());
1529 }
1530 let n = &mut nodes[id.0 as usize];
1531 n.span_kind = Some(kind);
1532 n.at = Some((end, end));
1533 n.text = normalize_ws(&text);
1534 n.genoses = genoses;
1535 n.fields = fields;
1536 }
1537 Block::Character {
1538 lemma,
1539 onym,
1540 text,
1541 genoses,
1542 } => {
1543 let parent = cursor(§ions, &containers, root);
1544 let id = push(&mut nodes, Kind::Character, parent);
1545 let n = &mut nodes[id.0 as usize];
1546 n.lemma = Some(normalize_ws(&lemma)).filter(|l| !l.is_empty());
1547 n.onym = onym;
1548 n.text = normalize_ws(&text);
1549 n.genoses = genoses;
1550 last_block = Some(id);
1551 }
1552 Block::Parsed {
1553 lo,
1554 hi,
1555 onym,
1556 parsings,
1557 } => {
1558 let Some(flow) = last_flow else { continue };
1559 nodes[flow.0 as usize].declared_tokens.push(DeclaredToken {
1560 lo,
1561 hi,
1562 onym,
1563 parsings,
1564 });
1565 }
1566 Block::Periodos { lo, hi, id } => {
1567 let Some(flow) = last_flow else { continue };
1568 nodes[flow.0 as usize].declared_sentences.push((lo, hi, id));
1569 }
1570 Block::Milestone { scheme, value, at } => {
1571 let Some(flow) = last_flow else { continue };
1572 let at = if at == u32::MAX {
1576 nodes[flow.0 as usize].text.len() as u32
1577 } else {
1578 at
1579 };
1580 let parent = enclosing_span(&nodes, flow, at, at).unwrap_or(flow);
1581 let id = push(&mut nodes, Kind::Milestone, parent);
1582 let n = &mut nodes[id.0 as usize];
1583 n.at = Some((at, at));
1584 n.fields = vec![("scheme".to_string(), scheme), ("value".to_string(), value)];
1585 }
1586 Block::Annotate { fields } => {
1587 if let Some(b) = last_block {
1588 nodes[b.0 as usize].fields.extend(fields);
1589 }
1590 }
1591 Block::Wear { genoses } => {
1592 if let Some(b) = last_block {
1593 let own = &mut nodes[b.0 as usize].genoses;
1594 for g in genoses {
1595 if !own.contains(&g) {
1596 own.push(g);
1597 }
1598 }
1599 }
1600 }
1601 Block::Text { text } => {
1602 let text = normalize_ws(&text);
1603 if text.is_empty() {
1604 continue;
1605 }
1606 match containers.last() {
1607 Some(&open) => {
1608 let own = &mut nodes[open.0 as usize].text;
1609 if !own.is_empty() {
1610 own.push(' ');
1611 }
1612 own.push_str(&text);
1613 last_flow = Some(open);
1614 }
1615 None => {
1616 let parent = sections.last().copied().unwrap_or(root);
1617 let id = push(&mut nodes, Kind::Paragraph, parent);
1618 nodes[id.0 as usize].text = text;
1619 last_flow = Some(id);
1620 }
1621 }
1622 }
1623 Block::Open { kind, lemma } => {
1624 let kind = match kind {
1628 Container::Note {
1629 onym,
1630 family,
1631 margin,
1632 } => {
1633 let id = push(&mut nodes, family.kind(), root);
1634 let n = &mut nodes[id.0 as usize];
1635 n.onym = Some(onym.trim().to_string()).filter(|o| !o.is_empty());
1636 n.margin = margin;
1637 containers.push(id);
1638 last_flow = Some(id);
1639 continue;
1640 }
1641 other => other,
1642 };
1643 let parent = cursor(§ions, &containers, root);
1644 let (taxis, onym) = match &kind {
1645 Container::Entry { taxis, onym } => (*taxis, onym.clone()),
1646 Container::Sense { taxis } => (*taxis, None),
1647 _ => (None, None),
1648 };
1649 let (nkind, start) = match kind {
1650 Container::Blockquote => (Kind::Blockquote, None),
1651 Container::UnorderedList => (Kind::UnorderedList, None),
1652 Container::OrderedList { start } => (Kind::OrderedList, Some(start)),
1653 Container::Entry { .. } => (Kind::Entry, None),
1654 Container::Sense { .. } => (Kind::Sense, None),
1655 Container::Item => (
1656 match nodes[parent.0 as usize].kind {
1657 Kind::OrderedList => Kind::OrderedItem,
1658 _ => Kind::UnorderedItem,
1659 },
1660 None,
1661 ),
1662 Container::Note { .. } => unreachable!("handled above"),
1663 };
1664 let id = push(&mut nodes, nkind, parent);
1665 last_block = Some(id);
1666 nodes[id.0 as usize].lemma =
1667 lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
1668 if let Some(start) = start {
1669 nodes[id.0 as usize].start = start;
1670 }
1671 if taxis.is_some() {
1672 nodes[id.0 as usize].taxis = taxis;
1673 }
1674 if nkind == Kind::Entry {
1678 let n = &mut nodes[id.0 as usize];
1679 n.onym = onym
1680 .map(|o| normalize_ws(&o))
1681 .filter(|o| !o.is_empty())
1682 .or_else(|| n.lemma.clone());
1683 }
1684 if nkind == Kind::OrderedItem {
1685 let nth = nodes[parent.0 as usize]
1688 .children
1689 .iter()
1690 .filter(|&&c| nodes[c.0 as usize].kind == Kind::OrderedItem)
1691 .count() as i64;
1692 let start = nodes[parent.0 as usize].start;
1693 nodes[id.0 as usize].taxis = Some(start + nth - 1);
1694 }
1695 containers.push(id);
1696 }
1697 Block::Close { hypograph } => {
1698 if let Some(open) = containers.pop() {
1699 nodes[open.0 as usize].hypograph = hypograph
1700 .map(|h| normalize_ws(&h))
1701 .filter(|h| !h.is_empty());
1702 }
1703 }
1704 Block::NoteRef {
1708 onym,
1709 family,
1710 margin,
1711 at,
1712 } => {
1713 let parent = last_flow.unwrap_or(root);
1714 let kind = family.map(NoteFamily::kind).unwrap_or(Kind::Footnote);
1718 let id = push(&mut nodes, kind, parent);
1719 let n = &mut nodes[id.0 as usize];
1720 n.deixis = true;
1721 n.family_open = family.is_none();
1722 n.margin = margin;
1723 n.at = at.map(|a| (a, a));
1724 n.onym = Some(onym.trim().to_string()).filter(|o| !o.is_empty());
1725 }
1726 Block::Ref {
1727 target,
1728 text,
1729 internal,
1730 } => {
1731 let parent = last_flow.unwrap_or(root);
1732 let id = push(&mut nodes, Kind::Ref, parent);
1733 let n = &mut nodes[id.0 as usize];
1734 n.internal = internal;
1735 n.target = Some(target.trim().to_string()).filter(|t| !t.is_empty());
1736 n.text = text.map(|t| normalize_ws(&t)).unwrap_or_default();
1737 }
1738 Block::Cite { target, locator } => {
1739 let parent = last_flow.unwrap_or(root);
1740 let id = push(&mut nodes, Kind::Cit, parent);
1741 let n = &mut nodes[id.0 as usize];
1742 n.internal = true;
1743 n.target = Some(target.trim().to_string()).filter(|t| !t.is_empty());
1744 n.locator = locator
1745 .map(|l| l.trim().to_string())
1746 .filter(|l| !l.is_empty());
1747 }
1748 Block::Bib {
1749 key,
1750 text,
1751 fields,
1752 genus,
1753 } => {
1754 let parent = cursor(§ions, &containers, root);
1755 let id = push(&mut nodes, Kind::Bib, parent);
1756 last_block = Some(id);
1757 let n = &mut nodes[id.0 as usize];
1758 n.onym = Some(key.trim().to_string()).filter(|k| !k.is_empty());
1759 n.text = normalize_ws(&text);
1760 n.fields = fields
1761 .into_iter()
1762 .map(|(k, v)| (k.trim().to_string(), normalize_ws(&v)))
1763 .filter(|(k, v)| !k.is_empty() && !v.is_empty())
1764 .collect();
1765 n.genus = genus
1766 .map(|g| g.trim().to_string())
1767 .filter(|g| !g.is_empty());
1768 }
1769 Block::Anchor { onym } => {
1770 let parent = last_flow.unwrap_or(root);
1771 let id = push(&mut nodes, Kind::Anchor, parent);
1772 nodes[id.0 as usize].onym =
1773 Some(onym.trim().to_string()).filter(|o| !o.is_empty());
1774 }
1775 Block::Label { onym } => {
1776 let onym = onym.trim().to_string();
1777 if onym.is_empty() {
1778 continue;
1779 }
1780 let free = last_block
1786 .map(|b| nodes[b.0 as usize].onym.is_none())
1787 .unwrap_or(false);
1788 match (last_block, free) {
1789 (Some(b), true) => {
1790 nodes[b.0 as usize].onym = Some(onym);
1791 }
1792 _ => {
1793 let parent = last_flow.unwrap_or(root);
1794 let id = push(&mut nodes, Kind::Anchor, parent);
1795 nodes[id.0 as usize].onym = Some(onym);
1796 }
1797 }
1798 }
1799 Block::IndexMark { term } => {
1800 let parent = last_flow.unwrap_or(root);
1801 let id = push(&mut nodes, Kind::IndexMark, parent);
1802 nodes[id.0 as usize].onym = Some(quarb_term(&term)).filter(|t| !t.is_empty());
1803 }
1804 Block::Verbatim { lang, text } => {
1805 let parent = cursor(§ions, &containers, root);
1806 let id = push(&mut nodes, Kind::Verbatim, parent);
1807 last_block = Some(id);
1808 let n = &mut nodes[id.0 as usize];
1809 n.lang = lang.filter(|l| !l.is_empty());
1810 n.text = text;
1811 }
1812 Block::Table {
1813 lemma,
1814 headers,
1815 rows,
1816 } => {
1817 let parent = cursor(§ions, &containers, root);
1818 lower_table(&mut nodes, parent, lemma, headers, rows);
1819 }
1820 Block::Verse {
1821 lemma,
1822 strophes,
1823 hypograph,
1824 } => {
1825 let parent = cursor(§ions, &containers, root);
1826 let id = lower_verse(&mut nodes, parent, lemma, strophes, hypograph);
1827 last_flow = Some(id);
1828 last_block = Some(id);
1829 }
1830 }
1831 }
1832
1833 resolve_notes(&mut nodes);
1834 let onyms = resolve_refs(&mut nodes);
1835 flatten_prose(&mut nodes);
1836 let mut model = TextModel {
1837 nodes,
1838 root,
1839 caches: Default::default(),
1840 onyms,
1841 document_url: None,
1842 link_base: None,
1843 document_path: None,
1844 cite_scheme: None,
1845 notes_omitted: false,
1846 orthography: None,
1847 bib_aliases: Default::default(),
1848 head: HeadMeta::default(),
1849 token_index: Default::default(),
1850 mentions: Vec::new(),
1851 tiered: false,
1852 bonds: None,
1853 tiers: Default::default(),
1854 given: Default::default(),
1855 ords: Default::default(),
1856 virt_fields: Default::default(),
1857 virt_character: Default::default(),
1858 virt_place: Default::default(),
1859 ann: Vec::new(),
1860 ann_names: Default::default(),
1861 ann_of: Default::default(),
1862 ann_deps: Default::default(),
1863 ann_edeps: Default::default(),
1864 };
1865 model.link_characters();
1866 model
1867 }
1868
1869 pub fn set_bib_aliases(&mut self, map: std::collections::HashMap<String, String>) {
1873 self.mutated();
1874 self.bib_aliases = map;
1875 }
1876
1877 pub fn set_document_url(&mut self, url: &str) {
1880 self.mutated();
1881 self.document_url = url::Url::parse(url).ok();
1882 }
1883
1884 pub fn set_link_base(&mut self, url: &str) {
1888 self.mutated();
1889 self.link_base = url::Url::parse(url).ok();
1890 }
1891
1892 pub fn set_document_path(&mut self, path: &str) {
1896 self.mutated();
1897 self.document_path = Some(path.to_string());
1898 }
1899
1900 pub fn omit_notes(&mut self, omit: bool) {
1905 self.mutated();
1906 self.notes_omitted = omit;
1907 }
1908
1909 pub fn lower_references(&mut self, standard: &str) -> Result<usize, String> {
1922 self.mutated();
1923 if !quarb::bibref::STANDARDS.contains(&standard) {
1924 return Err(format!(
1925 "unknown standard {standard:?} for references= — this build reads {}",
1926 quarb::bibref::STANDARDS.join(", ")
1927 ));
1928 }
1929 let mut entries: Vec<(NodeId, quarb::bibref::Reference)> = Vec::new();
1931 for i in 0..self.nodes.len() {
1932 let n = &self.nodes[i];
1933 if !matches!(
1936 n.kind,
1937 Kind::Paragraph | Kind::OrderedItem | Kind::UnorderedItem
1938 ) || n.deixis
1939 || self.note_body_of(NodeId(i as u64)).is_some()
1940 {
1941 continue;
1942 }
1943 if n.kind == Kind::Paragraph
1947 && n.parent.is_some_and(|p| {
1948 matches!(
1949 self.nodes[p.0 as usize].kind,
1950 Kind::OrderedItem | Kind::UnorderedItem
1951 )
1952 })
1953 {
1954 continue;
1955 }
1956 let text = n.prose.clone();
1957 if text.chars().filter(|c| c.is_alphabetic()).count() < 8 {
1958 continue;
1959 }
1960 if let Ok(Some(r)) = quarb::bibref::parse(standard, &text) {
1961 let dated = r.get("year").is_some() || r.get("author").is_some();
1964 if r.get("title").is_some() && dated {
1965 entries.push((NodeId(i as u64), r));
1966 }
1967 }
1968 }
1969 if entries.is_empty() {
1970 return Ok(0);
1971 }
1972 let mut census: Vec<(String, Option<String>, Option<String>)> = Vec::new();
1975 for (id, r) in &entries {
1976 let item = if self.nodes[id.0 as usize].kind == Kind::OrderedItem {
1980 Some(*id)
1981 } else {
1982 self.nodes[id.0 as usize]
1983 .parent
1984 .filter(|p| self.nodes[p.0 as usize].kind == Kind::OrderedItem)
1985 };
1986 let ordinal = item.and_then(|p| {
1987 let list = self.nodes[p.0 as usize].parent?;
1988 let siblings = &self.nodes[list.0 as usize].children;
1989 let pos = siblings.iter().position(|c| *c == p)?;
1990 Some((self.nodes[list.0 as usize].start + pos as i64).to_string())
1991 });
1992 let onym = r.label.clone().or(ordinal).unwrap_or_else(|| {
1993 let party = r
1994 .get("author")
1995 .map(|a| a.split([',', ' ']).next().unwrap_or(a).to_string())
1996 .unwrap_or_default();
1997 format!("{}{}", party, r.get("year").unwrap_or(""))
1998 });
1999 let n = &mut self.nodes[id.0 as usize];
2000 n.kind = Kind::Bib;
2001 n.onym = Some(onym.clone());
2002 n.fields = r.campi();
2003 n.genus = r.genus.map(str::to_string);
2004 n.text = n.prose.clone();
2005 census.push((
2006 onym,
2007 r.get("author").map(str::to_string),
2008 r.get("year").map(str::to_string),
2009 ));
2010 }
2011 let entry_ids: std::collections::HashSet<u64> =
2013 entries.iter().map(|(id, _)| id.0).collect();
2014 let mut new_cits: Vec<(NodeId, String, Option<String>)> = Vec::new();
2015 for i in 0..self.nodes.len() {
2016 let n = &self.nodes[i];
2017 if entry_ids.contains(&(i as u64)) || n.deixis {
2018 continue;
2019 }
2020 if !matches!(
2021 n.kind,
2022 Kind::Paragraph
2023 | Kind::UnorderedItem
2024 | Kind::OrderedItem
2025 | Kind::Footnote
2026 | Kind::Endnote
2027 ) || n.prose.is_empty()
2028 {
2029 continue;
2030 }
2031 for c in quarb::bibref::citations(&n.prose) {
2032 let target = match &c.key {
2033 quarb::bibref::CitationKey::Number(k) => Some(k.clone()),
2034 quarb::bibref::CitationKey::AuthorYear { party, year } => {
2035 quarb::bibref::resolve_author_year(party, year, &census).map(str::to_string)
2036 }
2037 };
2038 let Some(target) = target else { continue };
2039 new_cits.push((NodeId(i as u64), target, c.locator.clone()));
2040 }
2041 }
2042 for (parent, target, locator) in new_cits {
2043 let id = push(&mut self.nodes, Kind::Cit, parent);
2044 let n = &mut self.nodes[id.0 as usize];
2045 n.internal = true;
2046 n.target = Some(target);
2047 n.locator = locator;
2048 }
2049 for n in self.nodes.iter_mut() {
2051 if n.kind == Kind::Cit {
2052 n.ref_edge = None;
2053 n.dangling = false;
2054 }
2055 n.ref_cites.clear();
2056 }
2057 let onyms = resolve_refs(&mut self.nodes);
2058 self.onyms = onyms;
2059 if self.bib_aliases.is_empty() {
2062 self.bib_aliases = quarb::bibref::alias_map()
2063 .into_iter()
2064 .map(|(canon, en)| (en.to_string(), canon.to_string()))
2065 .collect();
2066 }
2067 Ok(entries.len())
2068 }
2069
2070 pub fn set_cite_scheme(&mut self, scheme: &str) {
2071 self.mutated();
2072 self.cite_scheme = Some(scheme.to_string());
2073 }
2074
2075 pub fn set_orthography(&mut self, table: &str) -> Result<(), String> {
2078 self.mutated();
2079 quarb::translit::modernize("", table)?;
2080 self.orthography = Some(table.to_string());
2081 Ok(())
2082 }
2083
2084 fn modern(&self, form: &str) -> String {
2087 match &self.orthography {
2088 Some(t) => quarb::translit::modernize(form, t).unwrap_or_else(|_| form.to_string()),
2089 None => form.to_string(),
2090 }
2091 }
2092
2093 fn lower(&self, form: &str) -> String {
2095 self.modern(form).to_lowercase()
2096 }
2097
2098 fn cite_table(&self) -> Vec<(NodeId, u32, NodeId)> {
2101 let mut scheme = self.cite_scheme.clone();
2102 let mut out = Vec::new();
2103 for i in 0..self.nodes.len() {
2104 let n = &self.nodes[i];
2105 if n.kind != Kind::Milestone {
2106 continue;
2107 }
2108 let this = n
2109 .fields
2110 .iter()
2111 .find(|(k, _)| k == "scheme")
2112 .map(|(_, v)| v.clone());
2113 let Some(this) = this else { continue };
2114 match &scheme {
2115 None => scheme = Some(this),
2116 Some(s) if *s != this => continue,
2117 _ => {}
2118 }
2119 let at = n.at.map(|(a, _)| a).unwrap_or(0);
2120 let mut block = NodeId(i as u64);
2121 while self.nodes[block.0 as usize].kind == Kind::Span
2122 || self.nodes[block.0 as usize].kind == Kind::Milestone
2123 {
2124 match self.nodes[block.0 as usize].parent {
2125 Some(p) => block = p,
2126 None => break,
2127 }
2128 }
2129 out.push((block, at, NodeId(i as u64)));
2130 }
2131 out.sort_by_key(|(b, at, _)| (b.0, *at));
2132 out
2133 }
2134
2135 fn mutated(&mut self) {
2144 self.caches = Default::default();
2145 self.ords = Default::default();
2146 self.ann_deps = Default::default();
2147 self.ann_edeps = Default::default();
2148 }
2149
2150 fn caches(&self) -> &Caches {
2152 self.caches.get_or_init(|| {
2153 let mut callout_of_body = std::collections::HashMap::new();
2154 for (i, n) in self.nodes.iter().enumerate() {
2155 if n.deixis
2156 && let Some(body) = n.note_edge
2157 {
2158 callout_of_body.entry(body).or_insert(NodeId(i as u64));
2159 }
2160 }
2161 Caches {
2162 cite_table: self.cite_table(),
2163 prose_set: self.prose_blocks().into_iter().collect(),
2164 callout_of_body,
2165 }
2166 })
2167 }
2168
2169 fn cite(&self, node: NodeId) -> Option<String> {
2170 let caches = self.caches();
2171 let table = &caches.cite_table;
2172 if table.is_empty() {
2173 return self.structural_cite(node);
2174 }
2175 let n = &self.nodes[node.0 as usize];
2176 let value = |m: NodeId| {
2177 self.nodes[m.0 as usize]
2178 .fields
2179 .iter()
2180 .find(|(k, _)| k == "value")
2181 .map(|(_, v)| v.clone())
2182 };
2183 let container = matches!(n.kind, Kind::Document | Kind::Section)
2185 || (n.kind != Kind::Span
2186 && n.kind != Kind::Milestone
2187 && n.kind != Kind::Sentence
2188 && !caches.prose_set.contains(&node)
2189 && !n.children.is_empty()
2190 && n.prose.is_empty());
2191 if container {
2192 let inside = |mut b: NodeId| {
2193 loop {
2194 if b == node {
2195 return true;
2196 }
2197 match self.nodes[b.0 as usize].parent {
2198 Some(p) => b = p,
2199 None => return false,
2200 }
2201 }
2202 };
2203 return table
2204 .iter()
2205 .find(|(b, _, _)| inside(*b))
2206 .and_then(|(_, _, m)| value(*m));
2207 }
2208 let body = if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis {
2210 Some(node)
2211 } else {
2212 self.note_body_of(node)
2213 };
2214 if let Some(body) = body {
2215 let callout = caches.callout_of_body.get(&body).copied();
2216 return callout.and_then(|c| self.cite(c));
2217 }
2218 let (block, at) = match n.kind {
2220 _ if n.deixis && n.at.is_some() => {
2221 (n.parent.unwrap_or(node), n.at.map(|(a, _)| a).unwrap_or(0))
2222 }
2223 Kind::Milestone | Kind::Span => {
2224 let mut b = node;
2225 while matches!(self.nodes[b.0 as usize].kind, Kind::Span | Kind::Milestone) {
2226 match self.nodes[b.0 as usize].parent {
2227 Some(p) => b = p,
2228 None => break,
2229 }
2230 }
2231 (b, n.at.map(|(a, _)| a).unwrap_or(0))
2232 }
2233 Kind::Sentence if n.derived => {
2234 let at = self
2235 .ann_of
2236 .get(&(node.0 as usize))
2237 .and_then(|rows| rows.first())
2238 .map(|&r| self.ann[r as usize].at)
2239 .unwrap_or(0);
2240 (n.parent.unwrap_or(node), at)
2241 }
2242 _ => (node, 0),
2243 };
2244 let _ = value;
2245 self.cite_at(block, at, node)
2246 }
2247
2248 fn cite_at(&self, block: NodeId, at: u32, fallback: NodeId) -> Option<String> {
2252 let caches = self.caches();
2253 let value = |m: NodeId| {
2254 self.nodes[m.0 as usize]
2255 .fields
2256 .iter()
2257 .find(|(k, _)| k == "value")
2258 .map(|(_, v)| v.clone())
2259 };
2260 caches
2261 .cite_table
2262 .iter()
2263 .rev()
2264 .find(|(b, a, _)| (b.0, *a) <= (block.0, at))
2265 .and_then(|(_, _, m)| value(*m))
2266 .or_else(|| self.structural_cite(fallback))
2267 }
2268
2269 fn structural_cite(&self, node: NodeId) -> Option<String> {
2273 let mut names: Vec<String> = Vec::new();
2274 let mut cur = Some(node);
2275 while let Some(n) = cur {
2276 let node = &self.nodes[n.0 as usize];
2277 if node.kind == Kind::Section
2278 && let Some(l) = &node.lemma
2279 && !l.is_empty()
2280 {
2281 names.push(l.clone());
2282 }
2283 cur = node.parent;
2284 }
2285 if names.is_empty() {
2286 return None;
2287 }
2288 names.reverse();
2289 Some(names.join(" / "))
2290 }
2291
2292 fn href_base(&self) -> Option<String> {
2295 if let Some(u) = &self.document_url {
2296 return Some(u.to_string());
2297 }
2298 self.document_path.clone()
2299 }
2300
2301 fn nearest_onym(&self, node: NodeId) -> Option<(String, bool)> {
2305 let mut cur = Some(node);
2306 let mut exact = true;
2307 while let Some(id) = cur {
2308 let n = &self.nodes[id.0 as usize];
2309 if n.kind != Kind::Document
2310 && !n.deixis
2311 && let Some(o) = &n.onym
2312 {
2313 return Some((o.clone(), exact));
2314 }
2315 exact = false;
2316 cur = n.parent;
2317 }
2318 None
2319 }
2320
2321 fn href(&self, node: NodeId) -> Option<String> {
2333 let base = self.href_base()?;
2334 let n = &self.nodes[node.0 as usize];
2335 if n.kind == Kind::Document {
2336 return Some(base);
2337 }
2338 let served = base.split(['?', '#']).next().unwrap_or(&base);
2339 let last = served.rsplit('/').next().unwrap_or(served);
2340 let html = served.ends_with('/')
2341 || !last.contains('.')
2342 || last.ends_with(".html")
2343 || last.ends_with(".htm");
2344 if !html {
2345 return Some(base);
2346 }
2347 let mut out = base;
2348 let (anchor, exact) = match self.nearest_onym(node) {
2349 Some((a, exact)) => (Some(a), exact),
2350 None => (None, false),
2351 };
2352 if let Some(a) = &anchor {
2353 out.push('#');
2354 out.push_str(a);
2355 }
2356 if !exact {
2357 let words: Vec<&str> = n.prose.split_whitespace().collect();
2358 if !words.is_empty() {
2359 use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
2360 let enc =
2361 |ws: &[&str]| utf8_percent_encode(&ws.join(" "), NON_ALPHANUMERIC).to_string();
2362 if anchor.is_none() {
2363 out.push('#');
2364 }
2365 out.push_str(":~:text=");
2366 if words.len() <= 12 {
2367 out.push_str(&enc(&words));
2368 } else {
2369 out.push_str(&enc(&words[..6]));
2370 out.push(',');
2371 out.push_str(&enc(&words[words.len() - 6..]));
2372 }
2373 }
2374 }
2375 Some(out)
2376 }
2377
2378 fn document_title(&self) -> Option<String> {
2381 if let Some(t) = &self.head.title {
2382 return Some(t.clone());
2383 }
2384 self.nodes[self.root.0 as usize]
2385 .children
2386 .iter()
2387 .map(|c| &self.nodes[c.0 as usize])
2388 .find(|n| n.kind == Kind::Section)
2389 .and_then(|n| n.lemma.clone())
2390 }
2391
2392 pub fn set_head_meta(&mut self, head: HeadMeta) {
2395 self.mutated();
2396 self.head = head;
2397 }
2398
2399 pub fn head_meta(&self) -> &HeadMeta {
2401 &self.head
2402 }
2403
2404 pub fn plain_text(&self) -> String {
2411 let mut out = String::new();
2412 let mut stack = vec![self.root];
2413 while let Some(id) = stack.pop() {
2414 let n = &self.nodes[id.0 as usize];
2415 if let Some(l) = &n.lemma
2416 && !l.is_empty()
2417 {
2418 out.push_str(l);
2419 out.push('\n');
2420 }
2421 if !n.prose.is_empty() && n.kind != Kind::Document {
2425 out.push_str(&n.prose);
2426 out.push('\n');
2427 }
2428 for c in n.children.iter().rev() {
2429 stack.push(*c);
2430 }
2431 }
2432 out
2433 }
2434
2435 pub fn node_count(&self) -> usize {
2439 self.nodes.len()
2440 }
2441
2442 pub fn parse_plain(text: &str) -> Self {
2446 let mut blocks = Vec::new();
2447 let mut para: Vec<&str> = Vec::new();
2448 for line in text.lines() {
2449 if line.trim().is_empty() {
2450 if !para.is_empty() {
2451 blocks.push(Block::Paragraph {
2452 text: para.join(" "),
2453 });
2454 para.clear();
2455 }
2456 } else {
2457 para.push(line);
2458 }
2459 }
2460 if !para.is_empty() {
2461 blocks.push(Block::Paragraph {
2462 text: para.join(" "),
2463 });
2464 }
2465 Self::build(blocks)
2466 }
2467
2468 pub fn locator(&self, node: NodeId) -> String {
2472 if let Some(v) = tier::virt(node) {
2473 return self.v_locator(v);
2474 }
2475 let mut segments = Vec::new();
2476 let mut cur = Some(node);
2477 while let Some(id) = cur {
2478 let n = &self.nodes[id.0 as usize];
2479 if let Some(name) = n.kind.name() {
2480 segments.push(self.segment(id, name));
2481 }
2482 cur = n.parent;
2483 }
2484 segments.reverse();
2485 format!("/{}", segments.join("/"))
2486 }
2487
2488 fn segment(&self, node: NodeId, name: &str) -> String {
2489 let Some(parent) = self.nodes[node.0 as usize].parent else {
2490 return name.to_string();
2491 };
2492 let siblings = &self.nodes[parent.0 as usize].children;
2493 let same_name: Vec<NodeId> = siblings
2494 .iter()
2495 .copied()
2496 .filter(|&s| self.nodes[s.0 as usize].kind == self.nodes[node.0 as usize].kind)
2497 .collect();
2498 if same_name.len() > 1 {
2499 let n = same_name.iter().position(|&s| s == node).unwrap() + 1;
2500 format!("{name}[{n}]")
2501 } else {
2502 name.to_string()
2503 }
2504 }
2505}
2506
2507fn resolve_notes(nodes: &mut [Node]) {
2515 let mut bodies: std::collections::HashMap<(Kind, String), NodeId> = Default::default();
2516 for (i, n) in nodes.iter().enumerate() {
2517 if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
2518 && !n.deixis
2519 && let Some(o) = &n.onym
2520 {
2521 bodies
2522 .entry((n.kind, o.clone()))
2523 .or_insert(NodeId(i as u64));
2524 }
2525 }
2526 for i in 0..nodes.len() {
2527 if !matches!(nodes[i].kind, Kind::Footnote | Kind::Endnote | Kind::Aside)
2528 || !nodes[i].deixis
2529 {
2530 continue;
2531 }
2532 let Some(onym) = nodes[i].onym.clone() else {
2533 nodes[i].dangling = true;
2534 continue;
2535 };
2536 let hit = if nodes[i].family_open {
2537 [Kind::Footnote, Kind::Endnote, Kind::Aside]
2540 .into_iter()
2541 .find_map(|k| bodies.get(&(k, onym.clone())).map(|b| (k, *b)))
2542 } else {
2543 bodies
2544 .get(&(nodes[i].kind, onym))
2545 .map(|b| (nodes[i].kind, *b))
2546 };
2547 match hit {
2548 Some((family, body)) => {
2549 nodes[i].kind = family;
2550 nodes[i].note_edge = Some(body);
2551 let callout = NodeId(i as u64);
2552 nodes[body.0 as usize].cites.push(callout);
2553 }
2554 None => nodes[i].dangling = true,
2555 }
2556 }
2557}
2558
2559fn quarb_term(term: &str) -> String {
2563 normalize_ws(term.split('|').next().unwrap_or(term))
2564}
2565
2566fn cursor(sections: &[NodeId], containers: &[NodeId], root: NodeId) -> NodeId {
2567 containers
2568 .last()
2569 .or(sections.last())
2570 .copied()
2571 .unwrap_or(root)
2572}
2573
2574impl TextModel {
2575 pub fn apply_cast(&mut self, rows: &[CastRow]) {
2589 self.mutated();
2590 for row in rows {
2591 let known = self
2592 .nodes
2593 .iter()
2594 .any(|n| n.kind == Kind::Character && n.onym.as_deref() == Some(row.id.as_str()));
2595 if known {
2596 continue;
2597 }
2598 let root = NodeId(0);
2599 let id = push(&mut self.nodes, Kind::Character, root);
2600 let n = &mut self.nodes[id.0 as usize];
2601 n.lemma = Some(row.name.clone()).filter(|l| !l.is_empty());
2602 n.onym = Some(row.id.clone());
2603 n.fields = row.fields.clone();
2604 }
2605 let patterns: Vec<(&CastRow, ®ex::Regex, usize)> = rows
2609 .iter()
2610 .filter_map(|r| r.pattern.as_ref().map(|p| (r, p, r.words.max(1))))
2611 .collect();
2612 let widest = patterns.iter().map(|(_, _, w)| *w).max().unwrap_or(0);
2613 if widest > 0 {
2614 let words = self.word_runs();
2615 for (k, (id, _, parent)) in words.iter().enumerate() {
2616 if self.has_token_field(*id, "prosopon") {
2617 continue;
2618 }
2619 let mut run = vec![words[k].1.as_str()];
2620 for (_, form, p) in words.iter().skip(k + 1).take(widest - 1) {
2621 if p != parent {
2622 break;
2623 }
2624 run.push(form.as_str());
2625 }
2626 let hit = patterns.iter().find(|(_, p, w)| {
2627 (1..=(*w).min(run.len()))
2628 .rev()
2629 .any(|n| p.is_match(&run[..n].join(" ")))
2630 });
2631 if let Some((row, _, _)) = hit {
2632 self.push_token_field(*id, "prosopon", row.id.clone());
2633 }
2634 }
2635 }
2636 let chains: Vec<(&CastRow, &str)> = rows
2640 .iter()
2641 .filter_map(|r| {
2642 r.fields
2643 .iter()
2644 .find(|(k, _)| k == "cluster" || k == "chain")
2645 .map(|(_, v)| (r, v.as_str()))
2646 })
2647 .collect();
2648 if !chains.is_empty() {
2649 let firsts: Vec<(NodeId, String)> = self
2650 .mentions
2651 .iter()
2652 .filter_map(|m| {
2653 let c = m.cluster.as_deref()?;
2654 let (row, _) = chains.iter().find(|(_, id)| *id == c)?;
2655 Some((*m.tokens.first()?, row.id.clone()))
2656 })
2657 .collect();
2658 for (t, id) in firsts {
2659 self.push_token_field(t, "prosopon", id);
2660 }
2661 }
2662 if !patterns.is_empty() {
2666 for i in 0..self.nodes.len() {
2667 let n = &self.nodes[i];
2668 if n.kind != Kind::Dialogue || n.fields.iter().any(|(k, _)| k == "prosopon") {
2669 continue;
2670 }
2671 let Some(label) = n.lemma.clone() else {
2672 continue;
2673 };
2674 if let Some((row, _, _)) = patterns.iter().find(|(_, p, _)| p.is_match(&label)) {
2675 self.nodes[i]
2676 .fields
2677 .push(("prosopon".to_string(), row.id.clone()));
2678 }
2679 }
2680 }
2681 self.link_characters();
2682 }
2683
2684 fn word_runs(&self) -> Vec<(NodeId, String, NodeId)> {
2688 if self.tiered {
2689 return self.tier_words();
2690 }
2691 self.ann_words()
2692 }
2693
2694 fn link_characters(&mut self) {
2698 self.mutated();
2699 let mut by_onym: std::collections::HashMap<String, NodeId> =
2700 std::collections::HashMap::new();
2701 for (i, n) in self.nodes.iter().enumerate() {
2702 if n.kind == Kind::Character
2703 && let Some(o) = &n.onym
2704 {
2705 by_onym.entry(o.clone()).or_insert(NodeId(i as u64));
2706 }
2707 }
2708 for n in &mut self.nodes {
2709 n.mentions.clear();
2710 }
2711 for i in 0..self.nodes.len() {
2712 if self.nodes[i].kind == Kind::Character {
2713 continue;
2714 }
2715 let target = self.nodes[i]
2716 .fields
2717 .iter()
2718 .find(|(k, _)| k == "prosopon")
2719 .and_then(|(_, v)| by_onym.get(v).copied());
2720 self.nodes[i].character = target;
2721 if let Some(c) = target {
2722 self.nodes[c.0 as usize].mentions.push(NodeId(i as u64));
2723 }
2724 }
2725 self.virt_character.clear();
2728 let mut virt: Vec<(NodeId, NodeId)> = self
2729 .virt_fields
2730 .iter()
2731 .filter_map(|(id, f)| {
2732 let v = f.iter().find(|(k, _)| k == "prosopon")?;
2733 Some((*id, *by_onym.get(&v.1)?))
2734 })
2735 .collect();
2736 virt.sort();
2737 for (id, c) in virt {
2738 self.virt_character.insert(id, c);
2739 self.nodes[c.0 as usize].mentions.push(id);
2740 }
2741 }
2742
2743 pub fn apply_places(&mut self, rows: &[CastRow]) {
2752 self.mutated();
2753 for row in rows {
2754 let known = self
2755 .nodes
2756 .iter()
2757 .any(|n| n.kind == Kind::Place && n.onym.as_deref() == Some(row.id.as_str()));
2758 if known {
2759 continue;
2760 }
2761 let root = NodeId(0);
2762 let id = push(&mut self.nodes, Kind::Place, root);
2763 let n = &mut self.nodes[id.0 as usize];
2764 n.lemma = Some(row.name.clone()).filter(|l| !l.is_empty());
2765 n.onym = Some(row.id.clone());
2766 n.fields = row.fields.clone();
2767 }
2768 let patterns: Vec<(&CastRow, ®ex::Regex, usize)> = rows
2769 .iter()
2770 .filter_map(|r| r.pattern.as_ref().map(|p| (r, p, r.words.max(1))))
2771 .collect();
2772 let widest = patterns.iter().map(|(_, _, w)| *w).max().unwrap_or(0);
2773 if widest > 0 {
2774 let words = self.word_runs();
2775 for (k, (id, _, parent)) in words.iter().enumerate() {
2776 if self.has_token_field(*id, "chora") {
2777 continue;
2778 }
2779 let mut run = vec![words[k].1.as_str()];
2780 for (_, form, p) in words.iter().skip(k + 1).take(widest - 1) {
2781 if p != parent {
2782 break;
2783 }
2784 run.push(form.as_str());
2785 }
2786 let hit = patterns.iter().find(|(_, p, w)| {
2787 (1..=(*w).min(run.len()))
2788 .rev()
2789 .any(|n| p.is_match(&run[..n].join(" ")))
2790 });
2791 if let Some((row, _, _)) = hit {
2792 self.push_token_field(*id, "chora", row.id.clone());
2793 }
2794 }
2795 }
2796 self.link_places();
2797 }
2798
2799 fn link_places(&mut self) {
2803 self.mutated();
2804 let mut by_onym: std::collections::HashMap<String, NodeId> =
2805 std::collections::HashMap::new();
2806 for (i, n) in self.nodes.iter().enumerate() {
2807 if n.kind == Kind::Place
2808 && let Some(o) = &n.onym
2809 {
2810 by_onym.entry(o.clone()).or_insert(NodeId(i as u64));
2811 }
2812 }
2813 for n in &mut self.nodes {
2814 if n.kind == Kind::Place {
2815 n.mentions.clear();
2816 }
2817 }
2818 for i in 0..self.nodes.len() {
2819 if self.nodes[i].kind == Kind::Place {
2820 continue;
2821 }
2822 let target = self.nodes[i]
2823 .fields
2824 .iter()
2825 .find(|(k, _)| k == "chora")
2826 .and_then(|(_, v)| by_onym.get(v).copied());
2827 self.nodes[i].place = target;
2828 if let Some(c) = target {
2829 self.nodes[c.0 as usize].mentions.push(NodeId(i as u64));
2830 }
2831 }
2832 self.virt_place.clear();
2833 let mut virt: Vec<(NodeId, NodeId)> = self
2834 .virt_fields
2835 .iter()
2836 .filter_map(|(id, f)| {
2837 let v = f.iter().find(|(k, _)| k == "chora")?;
2838 Some((*id, *by_onym.get(&v.1)?))
2839 })
2840 .collect();
2841 virt.sort();
2842 for (id, c) in virt {
2843 self.virt_place.insert(id, c);
2844 self.nodes[c.0 as usize].mentions.push(id);
2845 }
2846 }
2847
2848 pub fn tokenize(&mut self) {
2849 self.mutated();
2850 self.tokenize_with(None)
2851 }
2852
2853 fn is_prose_block(&self, id: usize) -> bool {
2857 let node = &self.nodes[id];
2858 let prose_block = match node.kind {
2859 Kind::Paragraph => !node.children.iter().any(|c| {
2864 let c = &self.nodes[c.0 as usize];
2865 c.kind == Kind::Sentence && !c.derived
2866 }),
2867 Kind::Sentence => !node.derived,
2868 Kind::UnorderedItem | Kind::OrderedItem | Kind::Stichos => true,
2869 Kind::Speech | Kind::Dialogue => true,
2870 Kind::Footnote | Kind::Endnote | Kind::Aside => {
2874 !node.deixis && !self.holds_prose_block(id)
2875 }
2876 _ => false,
2877 };
2878 prose_block && !node.prose.is_empty()
2879 }
2880
2881 fn holds_prose_block(&self, id: usize) -> bool {
2883 self.nodes[id].children.iter().any(|c| {
2884 let c = c.0 as usize;
2885 self.is_prose_block(c) || self.holds_prose_block(c)
2886 })
2887 }
2888
2889 fn prose_blocks(&self) -> Vec<NodeId> {
2902 let mut flow = Vec::new();
2903 let mut by_body: std::collections::BTreeMap<NodeId, Vec<NodeId>> = Default::default();
2904 for id in 0..self.nodes.len() {
2905 if !self.is_prose_block(id) {
2906 continue;
2907 }
2908 let block = NodeId(id as u64);
2909 match self.in_body(block) {
2910 Some(body) => by_body.entry(body).or_default().push(block),
2911 None => flow.push(block),
2912 }
2913 }
2914 if self.notes_omitted || by_body.is_empty() {
2915 return flow;
2916 }
2917 let mut after: std::collections::HashMap<NodeId, Vec<NodeId>> = Default::default();
2921 let mut trailing = Vec::new();
2922 for &body in by_body.keys() {
2923 let n = &self.nodes[body.0 as usize];
2924 let anchor = (n.kind != Kind::Endnote)
2925 .then(|| n.cites.first().and_then(|&c| self.block_of(c)))
2926 .flatten();
2927 match anchor {
2928 Some(block) => after.entry(block).or_default().push(body),
2929 None => trailing.push(body),
2930 }
2931 }
2932 let mut out = Vec::with_capacity(self.nodes.len());
2933 let mut seen: std::collections::HashSet<NodeId> = Default::default();
2934 fn emit(
2935 blocks: &[NodeId],
2936 by_body: &std::collections::BTreeMap<NodeId, Vec<NodeId>>,
2937 after: &std::collections::HashMap<NodeId, Vec<NodeId>>,
2938 seen: &mut std::collections::HashSet<NodeId>,
2939 out: &mut Vec<NodeId>,
2940 ) {
2941 for &b in blocks {
2942 out.push(b);
2943 if let Some(bodies) = after.get(&b) {
2944 for &body in bodies {
2945 if seen.insert(body) {
2946 emit(&by_body[&body], by_body, after, seen, out);
2947 }
2948 }
2949 }
2950 }
2951 }
2952 emit(&flow, &by_body, &after, &mut seen, &mut out);
2953 for body in trailing {
2954 if seen.insert(body) {
2955 emit(&by_body[&body], &by_body, &after, &mut seen, &mut out);
2956 }
2957 }
2958 for &body in by_body.keys() {
2961 if seen.insert(body) {
2962 emit(&by_body[&body], &by_body, &after, &mut seen, &mut out);
2963 }
2964 }
2965 out
2966 }
2967
2968 fn in_body(&self, node: NodeId) -> Option<NodeId> {
2971 let n = &self.nodes[node.0 as usize];
2972 if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis {
2973 return Some(node);
2974 }
2975 self.note_body_of(node)
2976 }
2977
2978 fn block_of(&self, node: NodeId) -> Option<NodeId> {
2980 let mut cur = Some(node);
2981 while let Some(c) = cur {
2982 if self.is_prose_block(c.0 as usize) {
2983 return Some(c);
2984 }
2985 cur = self.nodes[c.0 as usize].parent;
2986 }
2987 None
2988 }
2989
2990 fn note_body_of(&self, node: NodeId) -> Option<NodeId> {
2994 let mut cur = self.nodes[node.0 as usize].parent;
2995 while let Some(p) = cur {
2996 let n = &self.nodes[p.0 as usize];
2997 if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) && !n.deixis {
2998 return Some(p);
2999 }
3000 cur = n.parent;
3001 }
3002 None
3003 }
3004
3005 pub fn corpus_text(&self) -> String {
3009 self.prose_blocks()
3010 .iter()
3011 .map(|&b| self.nodes[b.0 as usize].prose.as_str())
3012 .collect::<Vec<_>>()
3013 .join("\n\n")
3014 }
3015
3016 pub fn is_tokenized(&self) -> bool {
3018 self.tiered || !self.ann.is_empty()
3019 }
3020
3021 pub fn tokenize_with(&mut self, bonds: Option<&syndesmos::Syndesmos>) {
3025 self.mutated();
3026 use unicode_segmentation::UnicodeSegmentation;
3027 self.token_index = Default::default();
3028 let declared = self.prose_blocks().iter().any(|b| {
3032 let n = &self.nodes[b.0 as usize];
3033 !n.declared_tokens.is_empty() || !n.declared_sentences.is_empty()
3034 });
3035 if !declared && !self.is_tokenized() {
3036 self.tiered = true;
3037 self.bonds = bonds.cloned();
3038 self.tiers.borrow_mut().clear();
3039 self.attribute_spans();
3040 return;
3041 }
3042 let mut n: u32 = 0;
3043 let mut sentence: u32 = 0;
3044 for id in self.tier_blocks() {
3045 if self.heading_text(id).is_some() {
3046 self.push_heading_sentence(id, &mut n, &mut sentence);
3047 continue;
3048 }
3049 let block = NodeId(id as u64);
3050 let prose = self.nodes[id].prose.clone();
3051 let declared_sentences = std::mem::take(&mut self.nodes[id].declared_sentences);
3058 let mut declared_tokens = std::mem::take(&mut self.nodes[id].declared_tokens);
3059 declared_tokens.sort_by_key(|t| t.lo);
3060 let mut spans = if declared_sentences.is_empty() {
3061 sentence_spans(&prose, bonds)
3062 } else {
3063 declared_sentences
3064 .iter()
3065 .map(|(lo, hi, _)| (*lo as usize, *hi as usize))
3066 .filter(|(lo, hi)| lo < hi && *hi <= prose.len())
3067 .collect()
3068 };
3069 if spans.is_empty() {
3070 spans.push((0, prose.len()));
3071 }
3072 let sentence_ids: Vec<Option<String>> = if declared_sentences.is_empty() {
3073 Vec::new()
3074 } else {
3075 declared_sentences
3076 .iter()
3077 .map(|(_, _, sid)| Some(sid.clone()))
3078 .collect()
3079 };
3080 let sentences: Vec<(usize, usize, u32, NodeId)> = spans
3081 .into_iter()
3082 .enumerate()
3083 .map(|(k, (lo, hi))| {
3084 sentence += 1;
3085 let node = self.push_derived_sentence(block, &prose[lo..hi], sentence);
3086 if let Some(Some(sid)) = sentence_ids.get(k) {
3087 self.nodes[node.0 as usize]
3088 .fields
3089 .push(("sent_id".to_string(), sid.clone()));
3090 }
3091 (lo, hi, sentence, node)
3092 })
3093 .collect();
3094 let mut by_onym: Vec<(String, NodeId)> = Vec::new();
3096 let mut heads: Vec<(NodeId, String)> = Vec::new();
3097 let mut next_declared = 0usize;
3098 let mut skip_until = 0usize;
3099 for (at, seg) in prose.split_word_bound_indices() {
3100 if at < skip_until {
3101 continue;
3102 }
3103 while next_declared < declared_tokens.len()
3107 && (declared_tokens[next_declared].lo as usize) < at
3108 {
3109 next_declared += 1;
3110 }
3111 if let Some(d) = declared_tokens.get(next_declared)
3112 && d.lo as usize == at
3113 && (d.hi as usize) > at
3114 && (d.hi as usize) <= prose.len()
3115 {
3116 let d = d.clone();
3117 next_declared += 1;
3118 skip_until = d.hi as usize;
3119 let form = &prose[d.lo as usize..d.hi as usize];
3120 n += 1;
3121 let (in_sentence, span, parent) = sentences
3122 .iter()
3123 .find(|(lo, hi, _, _)| *lo <= at && at < *hi)
3124 .or_else(|| sentences.last())
3125 .map(|(lo, hi, s, node)| (*s, (*lo as u32, *hi as u32), *node))
3126 .expect("a block has at least one sentence");
3127 let _ = span;
3128 let tok = self.push_parsed(parent, form, &d, n, in_sentence, at as u32);
3129 if let Some(o) = &d.onym {
3130 by_onym.push((o.clone(), tok));
3131 }
3132 if let Some(h) = d.parsings.first().and_then(|p| p.kephale.clone()) {
3133 heads.push((tok, h));
3134 }
3135 continue;
3136 }
3137 if seg.chars().all(char::is_whitespace) {
3138 continue;
3139 }
3140 n += 1;
3141 let (in_sentence, _, parent) = sentences
3142 .iter()
3143 .find(|(lo, hi, _, _)| *lo <= at && at < *hi)
3144 .or_else(|| sentences.last())
3145 .map(|(lo, hi, s, node)| (*s, (*lo as u32, *hi as u32), *node))
3146 .expect("a block has at least one sentence");
3147 self.push_plain_row(parent, seg, n, in_sentence, at as u32);
3148 }
3149 let linked: Vec<(NodeId, NodeId)> = heads
3151 .into_iter()
3152 .filter_map(|(tok, h)| {
3153 by_onym
3154 .iter()
3155 .find(|(o, _)| *o == h)
3156 .map(|(_, head)| (tok, *head))
3157 })
3158 .collect();
3159 self.link_declared_heads(&linked);
3160 }
3161 self.attribute_spans();
3162 }
3163
3164 fn attribute_spans(&mut self) {
3168 self.mutated();
3169 if self.tiered {
3170 self.attribute_spans_virtual();
3171 return;
3172 }
3173 let spans: Vec<NodeId> = (0..self.nodes.len())
3174 .filter(|&i| self.nodes[i].kind == Kind::Span && self.nodes[i].at.is_some())
3175 .map(|i| NodeId(i as u64))
3176 .collect();
3177 if spans.is_empty() {
3178 return;
3179 }
3180 for span in spans {
3181 let (lo, hi) = self.nodes[span.0 as usize].at.expect("filtered");
3182 let mut block = span;
3185 while self.nodes[block.0 as usize].kind == Kind::Span
3186 && let Some(p) = self.nodes[block.0 as usize].parent
3187 {
3188 block = p;
3189 }
3190 let covered: Vec<NodeId> = self
3191 .ann_under(block.0 as usize)
3192 .into_iter()
3193 .filter(|&r| {
3194 let at = self.ann[r as usize].at;
3195 lo <= at && at < hi
3196 })
3197 .map(ann::ann_id)
3198 .collect();
3199 self.nodes[span.0 as usize].span_tokens = covered;
3200 }
3201 }
3202
3203 fn push_heading_sentence(&mut self, block: usize, n: &mut u32, sentence: &mut u32) {
3215 use unicode_segmentation::UnicodeSegmentation;
3216 let Some(text) = self.heading_text(block).map(str::to_string) else {
3217 return;
3218 };
3219 *sentence += 1;
3220 let node = self.push_derived_sentence(NodeId(block as u64), &text, *sentence);
3221 let kids = &mut self.nodes[block].children;
3223 if let Some(pos) = kids.iter().position(|&c| c == node) {
3224 let id = kids.remove(pos);
3225 kids.insert(0, id);
3226 }
3227 for (at, seg) in text.split_word_bound_indices() {
3228 if seg.chars().all(char::is_whitespace) {
3229 continue;
3230 }
3231 *n += 1;
3232 self.push_plain_row(node, seg, *n, *sentence, at as u32);
3233 }
3234 }
3235
3236 fn push_derived_sentence(&mut self, block: NodeId, text: &str, ordinal: u32) -> NodeId {
3237 self.mutated();
3238 let node = push(&mut self.nodes, Kind::Sentence, block);
3239 let s = &mut self.nodes[node.0 as usize];
3240 s.text = text.trim().to_string();
3241 s.prose = s.text.clone();
3242 s.taxis = Some(ordinal as i64);
3243 s.derived = true;
3244 node
3245 }
3246
3247 pub fn annotate_conllu(&mut self, conllu: &str) -> Result<(), String> {
3261 self.mutated();
3262 if self.is_tokenized() {
3263 return Err("the document is already tokenized".into());
3264 }
3265 self.token_index = Default::default();
3266 let blocks = self.prose_blocks();
3267 let mut doc = String::new();
3269 let mut ranges: Vec<(usize, usize, NodeId)> = Vec::new();
3270 for (i, &b) in blocks.iter().enumerate() {
3271 if i > 0 {
3272 doc.push_str("\n\n");
3273 }
3274 let lo = doc.len();
3275 doc.push_str(&self.nodes[b.0 as usize].prose);
3276 ranges.push((lo, doc.len(), b));
3277 }
3278 if ranges.is_empty() {
3279 return Err("the document has no prose to annotate".into());
3280 }
3281 let parsed = parse_conllu(conllu)?;
3282 let mut cursor = 0usize;
3283 let mut block_at = 0usize;
3284 let mut n: u32 = 0;
3285 let mut headings: Vec<usize> = (0..self.nodes.len())
3288 .filter(|&i| self.heading_text(i).is_some())
3289 .collect();
3290 headings.reverse();
3291 let mut sentence_no: u32 = 0;
3292 let mut entered: Option<usize> = None;
3293 for sent in parsed.sentences.iter() {
3294 let mut ordinal: u32 = 0;
3297 let mut made: Vec<(NodeId, &ConlluToken)> = Vec::new();
3298 let mut extents: Vec<(NodeId, usize, usize)> = Vec::new();
3303 let mut nodes_by_block: Vec<(NodeId, NodeId)> = Vec::new();
3304 let mut range: Option<(usize, usize, u32, String, String)> = None;
3307 for tok in &sent.tokens {
3308 if !tok.empty && tok.form.trim().is_empty() {
3311 continue;
3312 }
3313 let (lo, hi, mwt) = if tok.empty {
3314 (cursor, cursor, None)
3315 } else if let Some((lo, hi, until, id, form)) = &range
3316 && tok.range.is_none()
3317 && tok.ord <= *until
3318 {
3319 (*lo, *hi, Some((id.clone(), form.clone())))
3320 } else {
3321 range = None;
3322 while doc[cursor..].starts_with(char::is_whitespace) {
3323 cursor += doc[cursor..].chars().next().map_or(0, char::len_utf8);
3324 }
3325 if !doc[cursor..].starts_with(tok.form.as_str()) {
3326 let seen: String = doc[cursor..].chars().take(24).collect();
3327 return Err(format!(
3328 "CoNLL-U sentence {}, token {}: {:?} does not align with the text at offset {cursor} ({seen:?})",
3329 sentence_no + 1,
3330 tok.id,
3331 tok.form
3332 ));
3333 }
3334 let lo = cursor;
3335 cursor += tok.form.len();
3336 if let Some((_, b)) = tok.range {
3337 range = Some((lo, cursor, b, tok.id.clone(), tok.form.clone()));
3338 continue;
3339 }
3340 (lo, cursor, None)
3341 };
3342 if !tok.empty {
3343 while block_at + 1 < ranges.len() && lo >= ranges[block_at].1 {
3344 block_at += 1;
3345 }
3346 }
3347 let (blo, _, block) = ranges[block_at];
3348 if entered != Some(block_at) {
3349 while let Some(&h) = headings.last()
3351 && h < block.0 as usize
3352 {
3353 self.push_heading_sentence(h, &mut n, &mut sentence_no);
3354 headings.pop();
3355 }
3356 entered = Some(block_at);
3357 }
3358 if ordinal == 0 {
3359 sentence_no += 1;
3360 ordinal = sentence_no;
3361 }
3362 if !tok.empty {
3363 n += 1;
3364 }
3365 let sentence_node = match nodes_by_block.iter().find(|(b, _)| *b == block) {
3366 Some((_, s)) => *s,
3367 None => {
3368 let s = self.push_derived_sentence(block, "", ordinal);
3369 nodes_by_block.push((block, s));
3370 s
3371 }
3372 };
3373 let node = self.push_annotated(
3374 sentence_node,
3375 tok,
3376 if tok.empty { 0 } else { n },
3377 ordinal,
3378 mwt,
3379 );
3380 if !tok.empty {
3381 match extents.iter_mut().find(|(b, _, _)| *b == block) {
3382 Some(e) => {
3383 e.1 = e.1.min(lo - blo);
3384 e.2 = e.2.max(hi - blo);
3385 }
3386 None => extents.push((block, lo - blo, hi - blo)),
3387 }
3388 if let Some(row) = ann::ann_row(node) {
3389 self.ann[row as usize].at = (lo - blo) as u32;
3390 }
3391 }
3392 made.push((node, tok));
3393 }
3394 for (block, s) in &nodes_by_block {
3397 if let Some((_, lo, hi)) = extents.iter().find(|(b, _, _)| b == block) {
3398 let text = self.nodes[block.0 as usize].prose[*lo..*hi]
3399 .trim()
3400 .to_string();
3401 let sn = &mut self.nodes[s.0 as usize];
3402 sn.text = text.clone();
3403 sn.prose = text;
3404 }
3405 }
3406 self.link_sentence(&made);
3407 }
3408 while let Some(h) = headings.pop() {
3409 self.push_heading_sentence(h, &mut n, &mut sentence_no);
3410 }
3411 let header = parsed
3412 .globals
3413 .iter()
3414 .find(|(k, _)| k == "Entity")
3415 .map(|(_, v)| v.as_str());
3416 self.decode_mentions(header);
3417 Ok(())
3418 }
3419
3420 pub fn parse_conllu_text(text: &str) -> Result<Self, String> {
3427 Self::from_conllu_set(&[(None, text)], false)
3428 }
3429
3430 pub fn parse_conllu_corpus(text: &str) -> Result<Self, String> {
3437 Self::from_conllu_set(&[(None, text)], true)
3438 }
3439
3440 pub fn parse_conllu_text_set(files: &[(&str, &str)]) -> Result<Self, String> {
3447 let named: Vec<(Option<&str>, &str)> = files.iter().map(|(n, t)| (Some(*n), *t)).collect();
3448 Self::from_conllu_set(&named, false)
3449 }
3450
3451 pub fn parse_conllu_corpus_set(files: &[(&str, &str)]) -> Result<Self, String> {
3456 let named: Vec<(Option<&str>, &str)> = files.iter().map(|(n, t)| (Some(*n), *t)).collect();
3457 Self::from_conllu_set(&named, true)
3458 }
3459
3460 fn from_conllu_set(files: &[(Option<&str>, &str)], tokens: bool) -> Result<Self, String> {
3461 let mut parsed: Vec<(Option<&str>, ConlluDoc)> = Vec::with_capacity(files.len());
3462 for (name, text) in files {
3463 let doc = parse_conllu(text).map_err(|e| match name {
3464 Some(n) => format!("{n}: {e}"),
3465 None => e,
3466 })?;
3467 parsed.push((*name, doc));
3468 }
3469 let mut nodes = vec![Node::new(Kind::Document, None)];
3470 let root = NodeId(0);
3471 let mut sentences: Vec<(NodeId, usize, usize)> = Vec::new();
3473 for (fi, (name, doc)) in parsed.iter().enumerate() {
3474 let (base, doc_level) = match name {
3475 Some(n) => {
3476 let sec = push(&mut nodes, Kind::Section, root);
3477 nodes[sec.0 as usize].lemma = Some(n.to_string());
3478 nodes[sec.0 as usize].level = Some(1);
3479 (sec, 2)
3480 }
3481 None => (root, 1),
3482 };
3483 let mut section: Option<NodeId> = None;
3484 let mut para: Option<NodeId> = None;
3485 for (si, s) in doc.sentences.iter().enumerate() {
3486 if let Some(id) = &s.newdoc {
3487 let sec = push(&mut nodes, Kind::Section, base);
3488 nodes[sec.0 as usize].lemma = id.clone();
3489 nodes[sec.0 as usize].level = Some(doc_level);
3490 section = Some(sec);
3491 para = None;
3492 }
3493 if s.newpar.is_some() {
3494 let p = push(&mut nodes, Kind::Paragraph, section.unwrap_or(base));
3495 para = Some(p);
3496 }
3497 let parent = para.or(section).unwrap_or(base);
3498 let sent = push(&mut nodes, Kind::Sentence, parent);
3499 nodes[sent.0 as usize].taxis = Some(sentences.len() as i64 + 1);
3500 let text = s
3501 .comments
3502 .iter()
3503 .find(|(k, _)| k == "text")
3504 .map(|(_, v)| v.clone())
3505 .unwrap_or_else(|| conllu_surface(&s.tokens));
3506 nodes[sent.0 as usize].text = text;
3507 nodes[sent.0 as usize].fields = s.comments.clone();
3508 sentences.push((sent, fi, si));
3509 }
3510 }
3511 flatten_prose(&mut nodes);
3512 let mut model = TextModel {
3513 nodes,
3514 root,
3515 caches: Default::default(),
3516 onyms: Default::default(),
3517 document_url: None,
3518 link_base: None,
3519 document_path: None,
3520 cite_scheme: None,
3521 notes_omitted: false,
3522 orthography: None,
3523 bib_aliases: Default::default(),
3524 head: HeadMeta::default(),
3525 token_index: Default::default(),
3526 mentions: Vec::new(),
3527 tiered: false,
3528 bonds: None,
3529 tiers: Default::default(),
3530 given: Default::default(),
3531 ords: Default::default(),
3532 virt_fields: Default::default(),
3533 virt_character: Default::default(),
3534 virt_place: Default::default(),
3535 ann: Vec::new(),
3536 ann_names: Default::default(),
3537 ann_of: Default::default(),
3538 ann_deps: Default::default(),
3539 ann_edeps: Default::default(),
3540 };
3541 if tokens {
3542 let mut n: u32 = 0;
3543 for (i, (sent, fi, si)) in sentences.into_iter().enumerate() {
3544 let s = &parsed[fi].1.sentences[si];
3545 let ordinal = i as u32 + 1;
3546 let len = model.nodes[sent.0 as usize].prose.len() as u32;
3547 let mut made: Vec<(NodeId, &ConlluToken)> = Vec::new();
3548 let mut cursor = 0usize;
3549 let mut range: Option<(u32, String, String)> = None;
3551 for tok in &s.tokens {
3552 if let Some((_, b)) = tok.range {
3553 range = Some((b, tok.id.clone(), tok.form.clone()));
3554 continue;
3555 }
3556 let mwt = match &range {
3557 Some((until, id, form)) if !tok.empty && tok.ord <= *until => {
3558 Some((id.clone(), form.clone()))
3559 }
3560 _ => {
3561 range = None;
3562 None
3563 }
3564 };
3565 if !tok.empty {
3566 n += 1;
3567 }
3568 let in_range = mwt.is_some();
3569 let node = model.push_annotated(
3570 sent,
3571 tok,
3572 if tok.empty { 0 } else { n },
3573 ordinal,
3574 mwt,
3575 );
3576 let _ = len;
3577 if !tok.empty
3581 && let Some(row) = ann::ann_row(node)
3582 {
3583 let surface = &model.nodes[sent.0 as usize].prose;
3584 if let Some(found) =
3585 surface[cursor.min(surface.len())..].find(tok.form.as_str())
3586 {
3587 let at = cursor + found;
3588 model.ann[row as usize].at = at as u32;
3589 if !in_range {
3590 cursor = at + tok.form.len();
3591 }
3592 }
3593 }
3594 made.push((node, tok));
3595 }
3596 model.link_sentence(&made);
3597 }
3598 let header = parsed
3601 .iter()
3602 .flat_map(|(_, d)| d.globals.iter())
3603 .find(|(k, _)| k == "Entity")
3604 .map(|(_, v)| v.as_str());
3605 model.decode_mentions(header);
3606 }
3607 Ok(model)
3608 }
3609}
3610
3611fn enclosing_span(nodes: &[Node], flow: NodeId, lo: u32, hi: u32) -> Option<NodeId> {
3616 let mut best: Option<NodeId> = None;
3617 let mut at = flow;
3618 loop {
3619 let next = nodes[at.0 as usize].children.iter().rev().find_map(|&c| {
3620 let n = &nodes[c.0 as usize];
3621 match (n.kind, n.at) {
3622 (Kind::Span, Some((slo, shi)))
3623 if slo <= lo && hi <= shi && (slo, shi) != (lo, hi) =>
3624 {
3625 Some(c)
3626 }
3627 _ => None,
3628 }
3629 });
3630 match next {
3631 Some(c) => {
3632 best = Some(c);
3633 at = c;
3634 }
3635 None => return best,
3636 }
3637 }
3638}
3639
3640fn field_values(fields: &[(String, String)], name: &str) -> Option<Value> {
3643 let vs: Vec<Value> = fields
3644 .iter()
3645 .filter(|(k, _)| k == name)
3646 .map(|(_, v)| Value::Str(v.clone()))
3647 .collect();
3648 match vs.len() {
3649 0 => None,
3650 1 => vs.into_iter().next(),
3651 _ => Some(Value::list(vs)),
3652 }
3653}
3654
3655fn push(nodes: &mut Vec<Node>, kind: Kind, parent: NodeId) -> NodeId {
3656 let id = NodeId(nodes.len() as u64);
3657 nodes.push(Node::new(kind, Some(parent)));
3658 nodes[parent.0 as usize].children.push(id);
3659 id
3660}
3661
3662fn lower_table(
3668 nodes: &mut Vec<Node>,
3669 parent: NodeId,
3670 lemma: Option<String>,
3671 headers: Option<Vec<String>>,
3672 rows: Vec<Vec<Cell>>,
3673) {
3674 let list = push(nodes, Kind::OrderedList, parent);
3675 {
3676 let n = &mut nodes[list.0 as usize];
3677 n.table = true;
3678 n.lemma = lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
3679 }
3680 for (i, row) in rows.into_iter().enumerate() {
3681 let item = push(nodes, Kind::OrderedItem, list);
3682 nodes[item.0 as usize].taxis = Some(i as i64 + 1);
3683 nodes[item.0 as usize].row = true;
3684 let cells = push(nodes, Kind::UnorderedList, item);
3685 for (j, cell) in row.into_iter().enumerate() {
3686 let value = normalize_ws(&cell.text);
3687 if value.is_empty() {
3688 continue;
3689 }
3690 let label = cell
3691 .label
3692 .as_deref()
3693 .or_else(|| headers.as_ref().and_then(|h| h.get(j)).map(|h| h.as_str()))
3694 .map(normalize_ws)
3695 .filter(|h| !h.is_empty());
3696 let cell_item = push(nodes, Kind::UnorderedItem, cells);
3697 let n = &mut nodes[cell_item.0 as usize];
3698 n.cell = true;
3699 n.lemma = label;
3700 n.text = value;
3701 }
3702 }
3703}
3704
3705fn lower_verse(
3711 nodes: &mut Vec<Node>,
3712 parent: NodeId,
3713 lemma: Option<String>,
3714 strophes: Vec<Vec<String>>,
3715 hypograph: Option<String>,
3716) -> NodeId {
3717 let verse = push(nodes, Kind::Verse, parent);
3718 {
3719 let n = &mut nodes[verse.0 as usize];
3720 n.lemma = lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
3721 n.hypograph = hypograph
3722 .map(|h| normalize_ws(&h))
3723 .filter(|h| !h.is_empty());
3724 }
3725 let mut line_no = 0i64;
3726 for (i, strophe) in strophes.into_iter().enumerate() {
3727 let sid = push(nodes, Kind::Strophe, verse);
3728 nodes[sid.0 as usize].taxis = Some(i as i64 + 1);
3729 for line in strophe {
3730 let line = line.trim_end().to_string();
3731 if line.trim().is_empty() {
3732 continue;
3733 }
3734 line_no += 1;
3735 let lid = push(nodes, Kind::Stichos, sid);
3736 let n = &mut nodes[lid.0 as usize];
3737 n.taxis = Some(line_no);
3738 n.text = line;
3739 }
3740 }
3741 verse
3742}
3743
3744fn flatten_prose(nodes: &mut [Node]) {
3754 for i in (0..nodes.len()).rev() {
3755 let inline_lemma = matches!(nodes[i].kind, Kind::UnorderedItem | Kind::OrderedItem);
3756 let mut lemma_part: Option<String> = None;
3757 let mut parts: Vec<String> = Vec::new();
3758 if let Some(lemma) = &nodes[i].lemma
3759 && !lemma.is_empty()
3760 {
3761 if inline_lemma {
3762 lemma_part = Some(lemma.clone());
3763 } else if matches!(nodes[i].kind, Kind::Paragraph | Kind::Dialogue) {
3764 } else {
3768 parts.push(lemma.clone());
3769 }
3770 }
3771 if !nodes[i].text.is_empty() {
3772 parts.push(nodes[i].text.clone());
3773 }
3774 let mut child_parts: Vec<String> = Vec::new();
3775 for &child in nodes[i].children.clone().iter() {
3776 if matches!(
3780 nodes[child.0 as usize].kind,
3781 Kind::Ref | Kind::Cit | Kind::Anchor | Kind::Span | Kind::Milestone
3782 ) {
3783 continue;
3784 }
3785 let prose = &nodes[child.0 as usize].prose;
3786 if !prose.is_empty() {
3787 child_parts.push(prose.clone());
3788 }
3789 }
3790 if nodes[i].kind == Kind::Verse {
3794 let joined = child_parts.join("\n\n");
3795 if !joined.is_empty() {
3796 parts.push(joined);
3797 }
3798 } else if nodes[i].kind == Kind::Paragraph
3799 && !child_parts.is_empty()
3800 && nodes[i]
3801 .children
3802 .iter()
3803 .all(|c| nodes[c.0 as usize].kind == Kind::Sentence)
3804 {
3805 parts.push(child_parts.join(" "));
3808 } else {
3809 parts.extend(child_parts);
3810 }
3811 if let Some(hypograph) = &nodes[i].hypograph
3812 && !hypograph.is_empty()
3813 {
3814 parts.push(hypograph.clone());
3815 }
3816 let mut prose = parts.join("\n");
3817 if let Some(lemma) = lemma_part {
3818 prose = if prose.is_empty() {
3819 lemma
3820 } else {
3821 format!("{lemma}: {prose}")
3822 };
3823 }
3824 nodes[i].prose = prose;
3825 }
3826}
3827
3828impl TextModel {
3829 fn grammata(&self, node: NodeId) -> String {
3835 let n = &self.nodes[node.0 as usize];
3836 let mut s = n.prose.as_str();
3837 if let Some(lemma) = &n.lemma
3838 && !lemma.is_empty()
3839 && let Some(rest) = s.strip_prefix(lemma.as_str())
3840 {
3841 s = rest
3842 .strip_prefix(": ")
3843 .or_else(|| rest.strip_prefix('\n'))
3844 .unwrap_or(rest);
3845 }
3846 if let Some(h) = &n.hypograph
3847 && !h.is_empty()
3848 && let Some(rest) = s.strip_suffix(h.as_str())
3849 {
3850 s = rest.strip_suffix('\n').unwrap_or(rest);
3851 }
3852 s.trim_end().to_string()
3853 }
3854}
3855
3856impl TextModel {
3857 fn abbreviation(&self, text: &str) -> Option<NodeId> {
3860 let text = text.trim();
3861 if text.is_empty() {
3862 return None;
3863 }
3864 let list = self.nodes.iter().position(|n| {
3865 matches!(n.kind, Kind::UnorderedList | Kind::OrderedList)
3866 && n.fields
3867 .iter()
3868 .any(|(k, v)| k == "kind" && v == "abbreviations")
3869 })?;
3870 self.nodes[list].children.iter().copied().find(|&c| {
3873 self.nodes[c.0 as usize]
3874 .lemma
3875 .as_deref()
3876 .is_some_and(|l| l.split(',').any(|s| s.trim() == text))
3877 })
3878 }
3879}
3880
3881impl AstAdapter for TextModel {
3882 fn estimate(&self, scope: &[&str], plan: &quarb::StepPlan<'_>) -> Option<quarb::Estimate> {
3885 quarb::adapter::arena_estimate(self, scope, plan, quarb::Latency::Memory, None)
3886 }
3887
3888 fn root(&self) -> NodeId {
3889 self.root
3890 }
3891
3892 fn walks_for_references(&self) -> bool {
3895 true
3896 }
3897
3898 fn children(&self, node: NodeId) -> Vec<NodeId> {
3899 if let Some(v) = tier::virt(node) {
3900 return self.v_children(v);
3901 }
3902 let mut c = self.nodes[node.0 as usize].children.clone();
3903 if self.virtual_block(node.0 as usize) {
3904 let sentences = self.tier_sentences(node.0 as usize);
3905 if self.heading_text(node.0 as usize).is_some() {
3906 c.splice(0..0, sentences);
3908 } else {
3909 c.extend(sentences);
3910 }
3911 }
3912 if let Some(rows) = self.ann_of.get(&(node.0 as usize)) {
3913 c.extend(rows.iter().map(|&r| ann::ann_id(r)));
3914 }
3915 c
3916 }
3917
3918 fn name(&self, node: NodeId) -> Option<String> {
3919 if let Some(v) = tier::virt(node) {
3920 return Self::v_name(v);
3921 }
3922 let n = &self.nodes[node.0 as usize];
3923 if n.kind == Kind::Span {
3924 return n.span_kind.clone();
3925 }
3926 n.kind.name().map(str::to_string)
3927 }
3928
3929 fn parent(&self, node: NodeId) -> Option<NodeId> {
3930 if let Some(v) = tier::virt(node) {
3931 return self.v_parent(v);
3932 }
3933 self.nodes[node.0 as usize].parent
3934 }
3935
3936 fn descendants_where(
3942 &self,
3943 node: NodeId,
3944 name: &str,
3945 property: &str,
3946 value: &Value,
3947 ) -> Option<Vec<(NodeId, usize)>> {
3948 if name != "token" || property != "lower" {
3949 return None;
3950 }
3951 let Value::Str(word) = value else {
3952 return None;
3953 };
3954 if word.is_empty() || !word.chars().all(|c| c.is_alphabetic() || "’'-".contains(c)) {
3955 return None;
3956 }
3957 if tier::virt(node).is_some() {
3958 return None;
3959 }
3960 let index = self.token_index.get_or_init(|| {
3961 if self.tiered {
3962 return self.tier_index();
3963 }
3964 let mut map: std::collections::HashMap<String, Vec<NodeId>> =
3965 std::collections::HashMap::new();
3966 for (i, r) in self.ann.iter().enumerate() {
3967 map.entry(self.lower(self.ann_names.get(r.form)))
3968 .or_default()
3969 .push(ann::ann_id(i as u32));
3970 }
3971 map
3972 });
3973 let Some(hits) = index.get(word.as_str()) else {
3974 return Some(Vec::new());
3975 };
3976 let mut out = Vec::new();
3977 for &t in hits {
3978 let mut depth = 0usize;
3980 let mut cur = t;
3981 while let Some(p) = self.any_parent(cur) {
3982 depth += 1;
3983 if p == node {
3984 out.push((t, depth));
3985 break;
3986 }
3987 cur = p;
3988 }
3989 }
3990 Some(out)
3991 }
3992
3993 fn traits(&self, node: NodeId) -> Vec<String> {
3997 if let Some(v) = tier::virt(node) {
3998 return self.v_traits(v);
3999 }
4000 let n = &self.nodes[node.0 as usize];
4001 let mut out = Vec::new();
4002 if n.kind == Kind::Document {
4010 for (ns, t) in self
4011 .head
4012 .tags
4013 .iter()
4014 .map(|t| ("tag", t))
4015 .chain(self.head.category.iter().map(|c| ("category", c)))
4016 {
4017 let name = HeadMeta::trait_name(t);
4018 if name.is_empty() {
4019 continue;
4020 }
4021 let name = format!("{ns}:{name}");
4022 if !out.contains(&name) {
4023 out.push(name);
4024 }
4025 }
4026 }
4027 if n.kind != Kind::Document
4032 && !n.deixis
4033 && !n.derived
4034 && !matches!(
4035 n.kind,
4036 Kind::IndexMark
4037 | Kind::Ref
4038 | Kind::Cit
4039 | Kind::Anchor
4040 | Kind::Strophe
4041 | Kind::Stichos
4042 | Kind::Span
4043 | Kind::Milestone
4044 | Kind::Character
4045 | Kind::Place
4046 )
4047 {
4048 out.push("block".to_string());
4049 }
4050 for g in &n.genoses {
4053 if !out.contains(g) {
4054 out.push(g.clone());
4055 }
4056 }
4057 if n.derived {
4061 out.push("derived".to_string());
4062 }
4063 if matches!(n.kind, Kind::Ref | Kind::Cit) && n.dangling {
4069 out.push("dangling".to_string());
4070 }
4071 if n.kind == Kind::Bib
4074 && let Some(g) = &n.genus
4075 {
4076 out.push(g.clone());
4077 }
4078 if n.kind == Kind::Anchor
4079 || (n.onym.is_some()
4080 && !n.deixis
4081 && !matches!(
4082 n.kind,
4083 Kind::Footnote
4084 | Kind::Endnote
4085 | Kind::Aside
4086 | Kind::IndexMark
4087 | Kind::Ref
4088 | Kind::Cit
4089 | Kind::Bib
4090 | Kind::Span
4091 | Kind::Milestone
4092 ))
4093 {
4094 out.push("target".to_string());
4095 }
4096 if n.kind == Kind::Cit && n.locator.is_some() {
4097 out.push("locator".to_string());
4098 }
4099 if matches!(n.kind, Kind::Footnote | Kind::Endnote | Kind::Aside) {
4100 if n.deixis {
4107 out.push("deixis".to_string());
4108 } else if n.kind != Kind::Aside {
4109 out.push("note".to_string());
4110 }
4111 if n.dangling {
4112 out.push("dangling".to_string());
4113 }
4114 }
4115 if n.table {
4116 out.push("table".to_string());
4117 }
4118 if n.row {
4119 out.push("row".to_string());
4120 }
4121 if n.cell {
4122 out.push("cell".to_string());
4123 }
4124 out
4125 }
4126
4127 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
4138 if let Some(v) = tier::virt(node) {
4139 return self.v_property(v, name);
4140 }
4141 let n = &self.nodes[node.0 as usize];
4142 if name == "cite" {
4144 return self.cite(node).map(Value::Str);
4145 }
4146 if n.kind == Kind::Document {
4149 let h = &self.head;
4150 match name {
4151 "title" | "lemma" => return h.title.clone().map(Value::Str),
4152 "description" => return h.description.clone().map(Value::Str),
4153 "author" => return h.author.clone().map(Value::Str),
4154 "category" => return h.category.clone().map(Value::Str),
4155 "tags" => {
4156 return (!h.tags.is_empty())
4157 .then(|| Value::list(h.tags.iter().cloned().map(Value::Str).collect()));
4158 }
4159 "published" => return h.published.clone(),
4160 "modified" => return h.modified.clone(),
4161 _ => {}
4162 }
4163 }
4164 if n.kind == Kind::Bib && !n.fields.is_empty() {
4170 let canon = self
4171 .bib_aliases
4172 .get(&name.to_lowercase())
4173 .map(String::as_str)
4174 .unwrap_or(name);
4175 if let Some((_, v)) = n.fields.iter().find(|(k, _)| k == canon) {
4176 return Some(Value::Str(v.clone()));
4177 }
4178 }
4179 match name {
4180 "href" => return self.href(node).map(Value::Str),
4184 "anchor" => return self.nearest_onym(node).map(|(a, _)| Value::Str(a)),
4185 "link" => {
4186 let href = self.href(node)?;
4187 let text = match self.default_value(node) {
4188 Some(Value::Str(t)) => t,
4189 _ => String::new(),
4190 };
4191 return Some(Value::Record(vec![
4192 (
4193 "title".to_string(),
4194 self.document_title().map(Value::Str).unwrap_or(Value::Null),
4195 ),
4196 ("href".to_string(), Value::Str(href)),
4197 ("text".to_string(), Value::Str(text)),
4198 ]));
4199 }
4200 _ => {}
4201 }
4202 match name {
4203 "id" if n.kind == Kind::Sentence => n
4208 .fields
4209 .iter()
4210 .find(|(k, _)| k == "sent_id")
4211 .map(|(_, v)| Value::Str(v.clone())),
4212 "lemma" | "title" => n.lemma.clone().map(Value::Str),
4213 "onym" => n.onym.clone().map(Value::Str),
4214 "term" if n.kind == Kind::IndexMark => n.onym.clone().map(Value::Str),
4216 "target" if matches!(n.kind, Kind::Ref | Kind::Cit) => n.target.clone().map(Value::Str),
4219 "locator" if n.kind == Kind::Cit => n.locator.clone().map(Value::Str),
4222 _ if n.kind == Kind::Bib => {
4226 let canon = self
4227 .bib_aliases
4228 .get(&name.to_lowercase())
4229 .map(String::as_str)
4230 .unwrap_or(name);
4231 n.fields
4232 .iter()
4233 .find(|(k, _)| k == canon)
4234 .map(|(_, v)| Value::Str(v.clone()))
4235 }
4236 "hypograph" | "attribution" => n.hypograph.clone().map(Value::Str),
4237 "taxis" | "ord" => n.taxis.map(Value::Int),
4238 "grammata" | "body" => {
4239 let g = self.grammata(node);
4240 if g.is_empty() {
4241 None
4242 } else {
4243 Some(Value::Str(g))
4244 }
4245 }
4246 "text" => Some(Value::Str(n.prose.clone())),
4247 _ if n.kind == Kind::Sentence => n
4255 .fields
4256 .iter()
4257 .find(|(k, _)| k == name)
4258 .or_else(|| n.fields.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)))
4259 .map(|(_, v)| Value::Str(v.clone())),
4260 _ if matches!(
4267 n.kind,
4268 Kind::Span
4269 | Kind::Speech
4270 | Kind::Dialogue
4271 | Kind::Paragraph
4272 | Kind::Milestone
4273 | Kind::Character
4274 | Kind::Place
4275 | Kind::UnorderedList
4276 | Kind::OrderedList
4277 ) && !n.fields.is_empty() =>
4278 {
4279 field_values(&n.fields, name)
4280 }
4281 _ => None,
4282 }
4283 }
4284
4285 fn default_value(&self, node: NodeId) -> Option<Value> {
4288 if let Some(v) = tier::virt(node) {
4289 return self.v_default_value(v);
4290 }
4291 let n = &self.nodes[node.0 as usize];
4292 if n.kind == Kind::IndexMark {
4293 return Some(Value::Str(n.onym.clone().unwrap_or_default()));
4296 }
4297 if n.deixis {
4298 return Some(Value::Str(match n.note_edge {
4302 Some(body) => self.nodes[body.0 as usize].prose.clone(),
4303 None => n.onym.clone().unwrap_or_default(),
4304 }));
4305 }
4306 if n.kind == Kind::Ref {
4307 if !n.text.is_empty() {
4312 return Some(Value::Str(n.text.clone()));
4313 }
4314 return Some(Value::Str(match n.ref_edge {
4315 Some(t) => {
4316 let b = &self.nodes[t.0 as usize];
4317 b.lemma.clone().unwrap_or_else(|| b.prose.clone())
4318 }
4319 None => n.target.clone().unwrap_or_default(),
4320 }));
4321 }
4322 if n.kind == Kind::Bib && n.prose.is_empty() && !n.fields.is_empty() {
4323 return Some(Value::Str(
4326 n.fields
4327 .iter()
4328 .map(|(_, v)| v.as_str())
4329 .collect::<Vec<_>>()
4330 .join(". "),
4331 ));
4332 }
4333 if n.kind == Kind::Cit {
4334 return Some(Value::Str(n.target.clone().unwrap_or_default()));
4338 }
4339 if n.kind == Kind::Anchor {
4340 return Some(Value::Str(n.onym.clone().unwrap_or_default()));
4343 }
4344 Some(Value::Str(n.prose.clone()))
4345 }
4346
4347 fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
4351 if let Some(v) = tier::virt(node) {
4352 return self.v_links(v);
4353 }
4354 let n = &self.nodes[node.0 as usize];
4355 if n.kind == Kind::Span
4358 && n.span_kind.as_deref() == Some("label")
4359 && let Some(item) = self.abbreviation(&n.text)
4360 {
4361 return vec![("abbreviation".to_string(), item)];
4362 }
4363 let character: Vec<(String, NodeId)> = n
4365 .character
4366 .map(|c| ("character".to_string(), c))
4367 .into_iter()
4368 .chain(n.place.map(|p| ("place".to_string(), p)))
4369 .collect();
4370 if !character.is_empty() && n.kind != Kind::Span {
4374 return character;
4375 }
4376 if let Some(t) = n.ref_edge {
4377 let label = if n.kind == Kind::Cit { "cit" } else { "ref" };
4380 return vec![(label.to_string(), t)];
4381 }
4382 if n.kind == Kind::Span && !(n.span_tokens.is_empty() && character.is_empty()) {
4385 return n
4386 .span_tokens
4387 .iter()
4388 .map(|&t| ("token".to_string(), t))
4389 .chain(character)
4390 .collect();
4391 }
4392 match n.note_edge {
4393 Some(body) => vec![(n.kind.name().unwrap_or("footnote").to_string(), body)],
4394 None => Vec::new(),
4395 }
4396 }
4397
4398 fn link_property(
4402 &self,
4403 source: NodeId,
4404 label: &str,
4405 target: NodeId,
4406 name: &str,
4407 ) -> Option<Value> {
4408 if name != "rel" {
4409 return None;
4410 }
4411 if let Some(row) = ann::ann_row(source) {
4412 return self.a_link_property(row, label, target);
4413 }
4414 if tier::virt(source).is_some() {
4415 return None;
4416 }
4417 None
4418 }
4419
4420 fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
4421 if let Some(row) = ann::ann_row(node) {
4422 return self.a_backlinks(row);
4423 }
4424 if tier::virt(node).is_some() {
4425 return Vec::new();
4426 }
4427 let n = &self.nodes[node.0 as usize];
4428 let mut out: Vec<(String, NodeId)> = n
4429 .ref_cites
4430 .iter()
4431 .map(|&c| {
4432 let label = if self.nodes[c.0 as usize].kind == Kind::Cit {
4433 "cit"
4434 } else {
4435 "ref"
4436 };
4437 (label.to_string(), c)
4438 })
4439 .collect();
4440 let label = n.kind.name().unwrap_or("footnote");
4441 out.extend(n.cites.iter().map(|&c| (label.to_string(), c)));
4442 let mention_label = if n.kind == Kind::Place {
4443 "place"
4444 } else {
4445 "character"
4446 };
4447 out.extend(n.mentions.iter().map(|&m| (mention_label.to_string(), m)));
4448 out
4449 }
4450
4451 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
4457 if tier::virt(node).is_some() {
4458 return None;
4459 }
4460 let n = &self.nodes[node.0 as usize];
4461 if n.kind == Kind::Span && n.span_kind.as_deref() == Some("label") {
4462 return (property == "abbreviation"
4463 && matches!(hint, None | Some("*") | Some("block")))
4464 .then(|| self.abbreviation(&n.text))
4465 .flatten();
4466 }
4467 if !matches!(n.kind, Kind::Ref | Kind::Cit) || property != "target" {
4468 return None;
4469 }
4470 let landed = n.ref_edge?;
4471 let is_point = self.nodes[landed.0 as usize].kind == Kind::Anchor;
4472 match hint {
4473 None | Some("*") => Some(landed),
4474 Some("point") => is_point.then_some(landed),
4475 Some("block") => {
4476 if !is_point {
4477 return Some(landed);
4478 }
4479 let mut at = self.nodes[landed.0 as usize].parent;
4481 while let Some(b) = at {
4482 let bn = &self.nodes[b.0 as usize];
4483 if bn.kind != Kind::Document
4484 && !matches!(bn.kind, Kind::Ref | Kind::Anchor | Kind::IndexMark)
4485 {
4486 return Some(b);
4487 }
4488 at = bn.parent;
4489 }
4490 None
4491 }
4492 Some(_) => None,
4493 }
4494 }
4495
4496 fn ref_property(&self, node: NodeId) -> Option<String> {
4498 if tier::virt(node).is_some() {
4499 return None;
4500 }
4501 let n = &self.nodes[node.0 as usize];
4502 if n.kind == Kind::Span && n.span_kind.as_deref() == Some("label") {
4503 return Some("abbreviation".to_string());
4504 }
4505 matches!(n.kind, Kind::Ref | Kind::Cit).then(|| "target".to_string())
4506 }
4507
4508 fn external_ref(
4512 &self,
4513 node: NodeId,
4514 property: &str,
4515 hint: Option<&str>,
4516 ) -> Option<quarb::ExternalRef> {
4517 if !matches!(hint, None | Some("*")) || tier::virt(node).is_some() {
4518 return None;
4519 }
4520 let n = &self.nodes[node.0 as usize];
4521 if n.kind != Kind::Ref || property != "target" || n.internal {
4522 return None;
4523 }
4524 let t = n.target.as_deref()?.trim();
4525 if t.is_empty() || t.starts_with('#') {
4526 return None;
4527 }
4528 let u = match url::Url::parse(t) {
4529 Ok(u) => u,
4530 Err(url::ParseError::RelativeUrlWithoutBase) => self
4531 .link_base
4532 .as_ref()
4533 .or(self.document_url.as_ref())?
4534 .join(t)
4535 .ok()?,
4536 Err(_) => return None,
4537 };
4538 if u.scheme() != "http" && u.scheme() != "https" {
4539 return None;
4540 }
4541 Some(quarb::ExternalRef::uri(u.as_str()))
4542 }
4543
4544 fn resolve_fragment(&self, _node: NodeId, fragment: &str) -> Option<NodeId> {
4547 self.onyms.get(fragment).copied()
4548 }
4549
4550 fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
4554 &["level", "lang", "form", "n"]
4555 }
4556
4557 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
4560 if let Some(v) = tier::virt(node) {
4561 return self.v_metadata(v, key);
4562 }
4563 let n = &self.nodes[node.0 as usize];
4564 if n.kind == Kind::Document
4567 && let Some(v) = self.head.declared_value(key)
4568 {
4569 return Some(v);
4570 }
4571 match key {
4572 "level" => n.level.map(|l| Value::Int(l as i64)),
4573 "lang" => n
4577 .lang
4578 .clone()
4579 .or_else(|| {
4580 let i = n.genoses.iter().position(|g| g == "foreign")?;
4581 n.genoses.get(i + 1).cloned()
4582 })
4583 .map(Value::Str),
4584 "at" if n.at.is_some() => n.at.map(|(lo, _)| Value::Int(lo as i64)),
4585 "n" | "sentence" if n.kind == Kind::Sentence => n.taxis.map(Value::Int),
4590 "at" | "end" if n.kind == Kind::Sentence => {
4594 let rows = self.ann_of.get(&(node.0 as usize))?;
4595 let (first, last) = (*rows.first()?, *rows.last()?);
4596 let block = self.ann[first as usize].block as usize;
4597 if block == node.0 as usize {
4598 return Some(Value::Int(if key == "at" {
4599 0
4600 } else {
4601 n.prose.chars().count() as i64
4602 }));
4603 }
4604 Some(Value::Int(if key == "at" {
4605 self.chars_to(block, self.ann[first as usize].at)
4606 } else {
4607 let r = &self.ann[last as usize];
4608 self.chars_to(block, r.at) + self.ann_names.get(r.form).chars().count() as i64
4609 }))
4610 }
4611 "resolved" if n.deixis => Some(Value::Bool(!n.dangling)),
4614 "resolved" if n.kind == Kind::Cit || (n.kind == Kind::Ref && n.internal) => {
4618 Some(Value::Bool(n.ref_edge.is_some()))
4619 }
4620 "form" if n.margin => Some(Value::Str("margin".to_string())),
4623 "genus" if n.kind == Kind::Bib => n.genus.clone().map(Value::Str),
4625 _ => None,
4626 }
4627 }
4628}