1use quarb::{AstAdapter, NodeId, Value};
41
42pub mod render;
43pub use render::{Render, render_node, render_nodes};
44
45#[derive(Debug, Clone, PartialEq)]
49pub enum Block {
50 Heading { level: u8, lemma: String },
53 Paragraph { text: String },
55 Text { text: String },
59 Open { kind: Container, lemma: Option<String> },
62 Close { hypograph: Option<String> },
65 Verbatim { lang: Option<String>, text: String },
68 Table {
73 lemma: Option<String>,
74 headers: Option<Vec<String>>,
75 rows: Vec<Vec<String>>,
76 },
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Container {
82 Blockquote,
83 UnorderedList,
84 OrderedList { start: i64 },
86 Item,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92enum Kind {
93 Document,
94 Section,
95 Paragraph,
96 Blockquote,
97 UnorderedList,
98 OrderedList,
99 UnorderedItem,
100 OrderedItem,
101 Verbatim,
102}
103
104impl Kind {
105 fn name(self) -> Option<&'static str> {
106 Some(match self {
107 Kind::Document => return None,
108 Kind::Section => "section",
109 Kind::Paragraph => "paragraph",
110 Kind::Blockquote => "blockquote",
111 Kind::UnorderedList => "unordered-list",
112 Kind::OrderedList => "ordered-list",
113 Kind::UnorderedItem => "unordered-item",
114 Kind::OrderedItem => "ordered-item",
115 Kind::Verbatim => "verbatim",
116 })
117 }
118}
119
120struct Node {
121 kind: Kind,
122 lemma: Option<String>,
123 hypograph: Option<String>,
124 taxis: Option<i64>,
125 level: Option<u8>,
127 lang: Option<String>,
129 start: i64,
132 text: String,
134 prose: String,
136 table: bool,
138 parent: Option<NodeId>,
139 children: Vec<NodeId>,
140}
141
142impl Node {
143 fn new(kind: Kind, parent: Option<NodeId>) -> Self {
144 Node {
145 kind,
146 lemma: None,
147 hypograph: None,
148 taxis: None,
149 level: None,
150 lang: None,
151 start: 1,
152 text: String::new(),
153 prose: String::new(),
154 table: false,
155 parent,
156 children: Vec::new(),
157 }
158 }
159}
160
161pub fn normalize_ws(s: &str) -> String {
165 s.split_whitespace().collect::<Vec<_>>().join(" ")
166}
167
168pub struct TextModel {
170 nodes: Vec<Node>,
171 root: NodeId,
172}
173
174impl TextModel {
175 pub fn build(blocks: Vec<Block>) -> Self {
182 let mut nodes = vec![Node::new(Kind::Document, None)];
183 let root = NodeId(0);
184 let mut sections: Vec<NodeId> = Vec::new();
186 let mut containers: Vec<NodeId> = Vec::new();
188
189 for block in blocks {
190 match block {
191 Block::Heading { level, lemma } => {
192 let lemma = normalize_ws(&lemma);
193 if !containers.is_empty() {
194 if !lemma.is_empty() {
197 let parent = *containers.last().unwrap();
198 let id = push(&mut nodes, Kind::Paragraph, parent);
199 nodes[id.0 as usize].text = lemma;
200 }
201 continue;
202 }
203 while let Some(&open) = sections.last() {
204 if nodes[open.0 as usize].level >= Some(level) {
205 sections.pop();
206 } else {
207 break;
208 }
209 }
210 let parent = sections.last().copied().unwrap_or(root);
211 let id = push(&mut nodes, Kind::Section, parent);
212 let n = &mut nodes[id.0 as usize];
213 n.lemma = Some(lemma);
214 n.level = Some(level);
215 sections.push(id);
216 }
217 Block::Paragraph { text } => {
218 let text = normalize_ws(&text);
219 if text.is_empty() {
220 continue;
221 }
222 let parent = cursor(§ions, &containers, root);
223 let id = push(&mut nodes, Kind::Paragraph, parent);
224 nodes[id.0 as usize].text = text;
225 }
226 Block::Text { text } => {
227 let text = normalize_ws(&text);
228 if text.is_empty() {
229 continue;
230 }
231 match containers.last() {
232 Some(&open) => {
233 let own = &mut nodes[open.0 as usize].text;
234 if !own.is_empty() {
235 own.push(' ');
236 }
237 own.push_str(&text);
238 }
239 None => {
240 let parent = sections.last().copied().unwrap_or(root);
241 let id = push(&mut nodes, Kind::Paragraph, parent);
242 nodes[id.0 as usize].text = text;
243 }
244 }
245 }
246 Block::Open { kind, lemma } => {
247 let parent = cursor(§ions, &containers, root);
248 let (nkind, start) = match kind {
249 Container::Blockquote => (Kind::Blockquote, None),
250 Container::UnorderedList => (Kind::UnorderedList, None),
251 Container::OrderedList { start } => (Kind::OrderedList, Some(start)),
252 Container::Item => (
253 match nodes[parent.0 as usize].kind {
254 Kind::OrderedList => Kind::OrderedItem,
255 _ => Kind::UnorderedItem,
256 },
257 None,
258 ),
259 };
260 let id = push(&mut nodes, nkind, parent);
261 nodes[id.0 as usize].lemma =
262 lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
263 if let Some(start) = start {
264 nodes[id.0 as usize].start = start;
265 }
266 if nkind == Kind::OrderedItem {
267 let nth = nodes[parent.0 as usize]
270 .children
271 .iter()
272 .filter(|&&c| nodes[c.0 as usize].kind == Kind::OrderedItem)
273 .count() as i64;
274 let start = nodes[parent.0 as usize].start;
275 nodes[id.0 as usize].taxis = Some(start + nth - 1);
276 }
277 containers.push(id);
278 }
279 Block::Close { hypograph } => {
280 if let Some(open) = containers.pop() {
281 nodes[open.0 as usize].hypograph =
282 hypograph.map(|h| normalize_ws(&h)).filter(|h| !h.is_empty());
283 }
284 }
285 Block::Verbatim { lang, text } => {
286 let parent = cursor(§ions, &containers, root);
287 let id = push(&mut nodes, Kind::Verbatim, parent);
288 let n = &mut nodes[id.0 as usize];
289 n.lang = lang.filter(|l| !l.is_empty());
290 n.text = text;
291 }
292 Block::Table {
293 lemma,
294 headers,
295 rows,
296 } => {
297 let parent = cursor(§ions, &containers, root);
298 lower_table(&mut nodes, parent, lemma, headers, rows);
299 }
300 }
301 }
302
303 flatten_prose(&mut nodes);
304 TextModel { nodes, root }
305 }
306
307 pub fn parse_plain(text: &str) -> Self {
311 let mut blocks = Vec::new();
312 let mut para: Vec<&str> = Vec::new();
313 for line in text.lines() {
314 if line.trim().is_empty() {
315 if !para.is_empty() {
316 blocks.push(Block::Paragraph {
317 text: para.join(" "),
318 });
319 para.clear();
320 }
321 } else {
322 para.push(line);
323 }
324 }
325 if !para.is_empty() {
326 blocks.push(Block::Paragraph {
327 text: para.join(" "),
328 });
329 }
330 Self::build(blocks)
331 }
332
333 pub fn locator(&self, node: NodeId) -> String {
337 let mut segments = Vec::new();
338 let mut cur = Some(node);
339 while let Some(id) = cur {
340 let n = &self.nodes[id.0 as usize];
341 if let Some(name) = n.kind.name() {
342 segments.push(self.segment(id, name));
343 }
344 cur = n.parent;
345 }
346 segments.reverse();
347 format!("/{}", segments.join("/"))
348 }
349
350 fn segment(&self, node: NodeId, name: &str) -> String {
351 let Some(parent) = self.nodes[node.0 as usize].parent else {
352 return name.to_string();
353 };
354 let siblings = &self.nodes[parent.0 as usize].children;
355 let same_name: Vec<NodeId> = siblings
356 .iter()
357 .copied()
358 .filter(|&s| self.nodes[s.0 as usize].kind == self.nodes[node.0 as usize].kind)
359 .collect();
360 if same_name.len() > 1 {
361 let n = same_name.iter().position(|&s| s == node).unwrap() + 1;
362 format!("{name}[{n}]")
363 } else {
364 name.to_string()
365 }
366 }
367}
368
369fn cursor(sections: &[NodeId], containers: &[NodeId], root: NodeId) -> NodeId {
372 containers
373 .last()
374 .or(sections.last())
375 .copied()
376 .unwrap_or(root)
377}
378
379fn push(nodes: &mut Vec<Node>, kind: Kind, parent: NodeId) -> NodeId {
380 let id = NodeId(nodes.len() as u64);
381 nodes.push(Node::new(kind, Some(parent)));
382 nodes[parent.0 as usize].children.push(id);
383 id
384}
385
386fn lower_table(
388 nodes: &mut Vec<Node>,
389 parent: NodeId,
390 lemma: Option<String>,
391 headers: Option<Vec<String>>,
392 rows: Vec<Vec<String>>,
393) {
394 let list = push(nodes, Kind::OrderedList, parent);
395 {
396 let n = &mut nodes[list.0 as usize];
397 n.table = true;
398 n.lemma = lemma.map(|l| normalize_ws(&l)).filter(|l| !l.is_empty());
399 }
400 for (i, row) in rows.into_iter().enumerate() {
401 let item = push(nodes, Kind::OrderedItem, list);
402 nodes[item.0 as usize].taxis = Some(i as i64 + 1);
403 let cells = push(nodes, Kind::UnorderedList, item);
404 for (j, cell) in row.into_iter().enumerate() {
405 let value = normalize_ws(&cell);
406 if value.is_empty() {
407 continue;
408 }
409 let header = headers
410 .as_ref()
411 .and_then(|h| h.get(j))
412 .map(|h| normalize_ws(h))
413 .filter(|h| !h.is_empty());
414 let text = match header {
415 Some(h) => format!("{h}: {value}"),
416 None => value,
417 };
418 let cell_item = push(nodes, Kind::UnorderedItem, cells);
419 nodes[cell_item.0 as usize].text = text;
420 }
421 }
422}
423
424fn flatten_prose(nodes: &mut [Node]) {
431 for i in (0..nodes.len()).rev() {
432 let mut parts: Vec<String> = Vec::new();
433 if let Some(lemma) = &nodes[i].lemma
434 && !lemma.is_empty()
435 {
436 parts.push(lemma.clone());
437 }
438 if !nodes[i].text.is_empty() {
439 parts.push(nodes[i].text.clone());
440 }
441 for &child in nodes[i].children.clone().iter() {
442 let prose = &nodes[child.0 as usize].prose;
443 if !prose.is_empty() {
444 parts.push(prose.clone());
445 }
446 }
447 if let Some(hypograph) = &nodes[i].hypograph
448 && !hypograph.is_empty()
449 {
450 parts.push(hypograph.clone());
451 }
452 nodes[i].prose = parts.join("\n");
453 }
454}
455
456impl AstAdapter for TextModel {
457 fn root(&self) -> NodeId {
458 self.root
459 }
460
461 fn children(&self, node: NodeId) -> Vec<NodeId> {
462 self.nodes[node.0 as usize].children.clone()
463 }
464
465 fn name(&self, node: NodeId) -> Option<String> {
466 self.nodes[node.0 as usize].kind.name().map(str::to_string)
467 }
468
469 fn parent(&self, node: NodeId) -> Option<NodeId> {
470 self.nodes[node.0 as usize].parent
471 }
472
473 fn traits(&self, node: NodeId) -> Vec<String> {
477 let n = &self.nodes[node.0 as usize];
478 let mut out = Vec::new();
479 if n.kind != Kind::Document {
480 out.push("block".to_string());
481 }
482 if n.table {
483 out.push("table".to_string());
484 }
485 out
486 }
487
488 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
492 let n = &self.nodes[node.0 as usize];
493 match name {
494 "lemma" => n.lemma.clone().map(Value::Str),
495 "hypograph" => n.hypograph.clone().map(Value::Str),
496 "taxis" => n.taxis.map(Value::Int),
497 "text" => Some(Value::Str(n.prose.clone())),
498 _ => None,
499 }
500 }
501
502 fn default_value(&self, node: NodeId) -> Option<Value> {
505 Some(Value::Str(self.nodes[node.0 as usize].prose.clone()))
506 }
507
508 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
511 let n = &self.nodes[node.0 as usize];
512 match key {
513 "level" => n.level.map(|l| Value::Int(l as i64)),
514 "lang" => n.lang.clone().map(Value::Str),
515 _ => None,
516 }
517 }
518}