mdbook_najan/
lib.rs

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		// Compile regex for custom markup.
20		let najan_regex = Regex::new(r"\{([^{}]*)}").unwrap();
21
22		// Get lexemes indexed by lemma.
23		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: &regex::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	// The first line is assumed to be a raw Najan transcription. Output it
102	// first in Najan script (by wrapping in {}) and then italicized.
103	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	// Middle rows should be glosses, which can be output as-is.
107	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	// The last line is assumed to be a single full sentence, to be quoted.
112	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}