1use std::{collections::HashMap, io::Read};
2
3use lexi::lexicon::{Lexeme, Lexicon};
4use mdbook::{
5 book::{Book, Chapter},
6 errors::Error,
7 preprocess::{Preprocessor, PreprocessorContext},
8 BookItem,
9};
10use regex::Regex;
11
12pub struct Najan {
13 najan_regex: Regex,
14 lexemes_by_lemma: HashMap<String, Lexeme>,
15}
16
17impl Najan {
18 pub fn new<R: Read>(lexicon_reader: R) -> Najan {
19 let najan_regex = Regex::new(r"\{([^{}]*)}").unwrap();
21
22 let lexicon = Lexicon::open(lexicon_reader).unwrap();
24 let lexemes_by_lemma = lexicon
25 .lexemes
26 .into_iter()
27 .map(|lexeme| (lexeme.lemma.clone(), lexeme))
28 .collect();
29
30 Najan {
31 najan_regex,
32 lexemes_by_lemma,
33 }
34 }
35
36 fn expand_najan(&self, ch: &mut Chapter) {
37 ch.content = self
38 .najan_regex
39 .replace_all(&ch.content, |captures: ®ex::Captures| -> String {
40 captures[1]
41 .split_whitespace()
42 .map(|word| self.expand_najan_word(word))
43 .collect::<Vec<_>>()
44 .join(" ")
45 })
46 .to_string();
47 }
48
49 fn expand_najan_word(&self, word: &str) -> String {
50 match self.lexemes_by_lemma.get(word) {
51 Some(lexeme) => {
52 let glosses = if lexeme.glosses.is_empty() {
53 String::new()
54 } else {
55 format!(" — {}", lexeme.glosses.join("; "))
56 };
57 let translation = lexeme
58 .translation
59 .as_ref()
60 .map(|translation| {
61 format!(
62 r#"<span class="najan-tooltip-translation">{translation}</span>"#
63 )
64 })
65 .unwrap_or_default();
66 format!(
67 r#"<span class="najan-tooltip"><span class="najan"><a href="./dictionary.html#{word}" target="_blank">{word}</a></span><span class="najan-tooltip-text"><span class="najan-tooltip-heading"><span class="najan">{word}</span> ⟨{word}⟩{glosses}</span>{translation}</span></span>"#
68 )
69 }
70 None => format!(
71 r#"<span class="najan" style="text-decoration: wavy red underline">{word}</span>"#
72 ),
73 }
74 }
75
76 fn expand_interlinear_gloss(&self, ch: &mut Chapter) {
77 let mut new_content = Vec::new();
78 let mut in_gloss = false;
79 let mut gloss = Vec::new();
80 for line in ch.content.lines() {
81 if line == "<gloss>" {
82 in_gloss = true;
83 } else if in_gloss {
84 if line == "</gloss>" {
85 new_content.push(generate_gloss(&gloss));
86 gloss.clear();
87 in_gloss = false;
88 } else {
89 gloss.push(line.to_owned());
90 }
91 } else {
92 new_content.push(line.to_owned());
93 }
94 }
95 ch.content = new_content.join("\n");
96 }
97}
98
99fn generate_gloss(gloss: &[String]) -> String {
100 let mut result = "<table class='gloss'>".to_string();
101 let mut colspan = gloss[0].len();
104 result.push_str(&generate_gloss_row(&gloss[0], "{", "}"));
105 result.push_str(&generate_gloss_row(&gloss[0], "<i>", "</i>"));
106 for row in gloss[1..gloss.len() - 1].iter() {
108 colspan = colspan.max(row.len());
109 result.push_str(&generate_gloss_row(row, "", ""));
110 }
111 result.push_str(&format!(
113 r#"<tr><td colspan="{colspan}">“{}”</td></tr></table>"#,
114 gloss.last().unwrap(),
115 ));
116 result
117}
118
119fn generate_gloss_row(row: &str, left: &str, right: &str) -> String {
120 format!(
121 "<tr>{}</tr>",
122 row.split('|')
123 .map(|word| format!("<td>{left}{}{right}</td>", word.trim()))
124 .collect::<Vec<String>>()
125 .join("")
126 )
127}
128
129impl Preprocessor for Najan {
130 fn name(&self) -> &str {
131 "najan-preprocessor"
132 }
133
134 fn run(
135 &self,
136 _ctx: &PreprocessorContext,
137 mut book: Book,
138 ) -> Result<Book, Error> {
139 book.for_each_mut(|item| {
140 if let BookItem::Chapter(ch) = item {
141 self.expand_interlinear_gloss(ch);
142 self.expand_najan(ch);
143 }
144 });
145 Ok(book)
146 }
147
148 fn supports_renderer(&self, renderer: &str) -> bool {
149 renderer != "not-supported"
150 }
151}