markdown_it/common/ruler.rs
1//! Plugin manager with dependency resolution.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt::{Debug, Formatter};
5use std::hash::Hash;
6use std::slice::Iter;
7
8use once_cell::sync::OnceCell;
9
10///
11/// Ruler allows you to implement a plugin system with dependency management and ensure that
12/// your dependencies are called in the correct order.
13///
14/// You can use it like this:
15/// ```
16/// use markdown_it::common::ruler::Ruler;
17///
18/// // this example prints "[ hello, world! ]",
19/// // where each token is printed by separate closure
20/// let mut chain = Ruler::<&str, fn (&mut String)>::new();
21///
22/// // define rules printing "hello" and "world"
23/// chain.add("hello", |s| s.push_str("hello"));
24/// chain.add("world", |s| s.push_str("world"));
25///
26/// // open bracket should be before "hello", and closing one after "world"
27/// chain.add("open_bracket", |s| s.push_str("[ ")).before("hello");
28/// chain.add("close_bracket", |s| s.push_str(" ]")).after("world");
29///
30/// // between "hello" and "world" we shall have a comma
31/// chain.add("comma", |s| s.push_str(", ")).after("hello").before("world");
32///
33/// // after "world" we should have "!" as a first rule, but ensure "world" exists first
34/// chain.add("bang", |s| s.push_str("!")).require("world").after("world").before_all();
35///
36/// // now we run this chain
37/// let mut result = String::new();
38/// for f in chain.iter() { f(&mut result); }
39/// assert_eq!(result, "[ hello, world! ]");
40/// ```
41///
42/// This data structure contains any number of elements (M, T), where T is any type and
43/// M (mark) is its identifier.
44///
45/// - `M` is used for ordering and dependency checking, it must implement `Eq + Copy + Hash + Debug`
46/// . Common choices for `M` are `u32`, `&'static str`, or a special `Symbol` type
47/// designed for this purpose.
48///
49/// - `T` is any user-defined type. It's usually a function or boxed trait.
50///
51pub struct Ruler<M, T> {
52 deps: Vec<RuleItem<M, T>>,
53 compiled: OnceCell<(Vec<usize>, Vec<T>)>,
54}
55
56impl<M, T> Ruler<M, T> {
57 pub fn new() -> Self {
58 Self::default()
59 }
60}
61
62impl<M: Eq + Hash + Copy + Debug, T: Clone> Ruler<M, T> {
63 /// Add a new rule identified by `mark` with payload `value`.
64 pub fn add(&mut self, mark: M, value: T) -> &mut RuleItem<M, T> {
65 self.compiled = OnceCell::new();
66 let dep = RuleItem::new(mark, value);
67 self.deps.push(dep);
68 self.deps.last_mut().unwrap()
69 }
70
71 /// Remove all rules identified by `mark`.
72 pub fn remove(&mut self, mark: M) {
73 self.deps.retain(|dep| !dep.marks.contains(&mark));
74 }
75
76 /// Check if there are any rules identified by `mark`.
77 pub fn contains(&mut self, mark: M) -> bool {
78 self.deps.iter().any(|dep| dep.marks.contains(&mark))
79 }
80
81 /// Ordered iteration through rules.
82 #[inline]
83 pub fn iter(&self) -> Iter<'_, T> {
84 self.compiled.get_or_init(|| self.compile()).1.iter()
85 }
86
87 fn compile(&self) -> (Vec<usize>, Vec<T>) {
88 // ID -> [RuleItem index]
89 let mut idhash = HashMap::<M, Vec<usize>>::new();
90
91 // RuleItem index -> [RuleItem index] - dependency graph, None if already inserted
92 let mut deps_graph = vec![HashSet::new(); self.deps.len()];
93
94 // additional level of indirection that takes into account item priority
95 let mut deps_order = vec![];
96 let mut beforeall_len = 0;
97 let mut afterall_len = 0;
98
99 // compiled result
100 let mut result = vec![];
101 let mut result_idx = vec![];
102
103 // track which rules have been added already
104 let mut deps_inserted = vec![false; self.deps.len()];
105 let mut deps_remaining = self.deps.len();
106
107 for (idx, dep) in self.deps.iter().enumerate() {
108 match dep.prio {
109 RuleItemPriority::Normal => {
110 deps_order.insert(deps_order.len() - afterall_len, idx);
111 }
112 RuleItemPriority::BeforeAll => {
113 deps_order.insert(beforeall_len, idx);
114 beforeall_len += 1;
115 }
116 RuleItemPriority::AfterAll => {
117 deps_order.insert(deps_order.len(), idx);
118 afterall_len += 1;
119 }
120 }
121 for mark in &dep.marks {
122 idhash.entry(*mark).or_default().push(idx);
123 }
124 }
125
126 // build dependency graph, replacing all after's with before's,
127 // i.e. B.after(A) -> A.before(B)
128 for idx in deps_order.iter().copied() {
129 let dep = self.deps.get(idx).unwrap();
130 for constraint in &dep.cons {
131 match constraint {
132 RuleItemConstraint::Before(v) => {
133 for depidx in idhash.entry(*v).or_default().iter() {
134 deps_graph.get_mut(*depidx).unwrap().insert(idx);
135 }
136 }
137 RuleItemConstraint::After(v) => {
138 for depidx in idhash.entry(*v).or_default().iter() {
139 deps_graph.get_mut(idx).unwrap().insert(*depidx);
140 }
141 }
142 RuleItemConstraint::Require(v) => {
143 assert!(
144 idhash.contains_key(v),
145 "missing dependency: {:?} requires {:?}",
146 dep.marks.first().unwrap(),
147 v
148 );
149 }
150 }
151 }
152 }
153
154 // now go through the deps and push whatever doesn't have any
155 'outer: while deps_remaining > 0 {
156 for idx in deps_order.iter().copied() {
157 let inserted = deps_inserted.get_mut(idx).unwrap();
158 if *inserted {
159 continue;
160 }
161
162 let dlist = deps_graph.get(idx).unwrap();
163 if dlist.is_empty() {
164 let dep = self.deps.get(idx).unwrap();
165 result.push(dep.value.clone());
166 result_idx.push(idx);
167 *inserted = true;
168 deps_remaining -= 1;
169 for d in deps_graph.iter_mut() {
170 d.remove(&idx);
171 }
172 continue 'outer;
173 }
174 }
175
176 #[cfg(debug_assertions)]
177 {
178 // check cycles in dependency graph;
179 // this is very suboptimal, but only used to generate a nice panic message.
180 // in release mode we'll just simply panic
181 for idx in deps_order.iter().copied() {
182 let mut seen = HashMap::new();
183 let mut vec = vec![idx];
184 while let Some(didx) = vec.pop() {
185 let dlist = deps_graph.get(didx).unwrap();
186 for x in dlist.iter() {
187 if seen.contains_key(x) {
188 continue;
189 }
190 vec.push(*x);
191 seen.insert(*x, didx);
192 if *x == idx {
193 let mut backtrack = vec![];
194 let mut curr = idx;
195 while !backtrack.contains(&curr) {
196 backtrack.push(curr);
197 curr = *seen.get(&curr).unwrap();
198 }
199 backtrack.push(curr);
200 let path = backtrack
201 .iter()
202 .rev()
203 .map(|x| {
204 format!(
205 "{:?}",
206 self.deps.get(*x).unwrap().marks.first().unwrap()
207 )
208 })
209 .collect::<Vec<String>>()
210 .join(" < ");
211 panic!("cyclic dependency: {}", path);
212 }
213 }
214 }
215 }
216 }
217
218 // if you see this in debug mode, report it as a bug
219 panic!("cyclic dependency: (use debug mode for more details)");
220 }
221
222 (result_idx, result)
223 }
224}
225
226impl<M: Eq + Hash + Copy + Debug, T: Clone> Debug for Ruler<M, T> {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 let vec: Vec<(usize, M)> = self
229 .compiled
230 .get_or_init(|| self.compile())
231 .0
232 .iter()
233 .map(|idx| (*idx, *self.deps.get(*idx).unwrap().marks.first().unwrap()))
234 .collect();
235
236 f.debug_struct("Ruler")
237 .field("deps", &self.deps)
238 .field("compiled", &vec)
239 .finish()
240 }
241}
242
243impl<M, T> Default for Ruler<M, T> {
244 fn default() -> Self {
245 Self {
246 deps: Vec::new(),
247 compiled: OnceCell::new(),
248 }
249 }
250}
251
252///
253/// Result of [Ruler::add](Ruler::add), allows to customize position of each rule.
254///
255pub struct RuleItem<M, T> {
256 marks: Vec<M>,
257 value: T,
258 prio: RuleItemPriority,
259 cons: Vec<RuleItemConstraint<M>>,
260}
261
262impl<M: std::fmt::Debug, T> Debug for RuleItem<M, T> {
263 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
264 f.debug_struct("RuleItem")
265 .field("marks", &self.marks)
266 .field("prio", &self.prio)
267 .field("cons", &self.cons)
268 .finish()
269 }
270}
271
272impl<M, T> RuleItem<M, T> {
273 fn new(mark: M, value: T) -> Self {
274 Self {
275 marks: vec![mark],
276 value,
277 prio: RuleItemPriority::Normal,
278 cons: vec![],
279 }
280 }
281}
282
283impl<M: Copy, T> RuleItem<M, T> {
284 /// Make sure this rule will be inserted before any rule defined by `mark` (if such rule exists).
285 /// ```
286 /// use markdown_it::common::ruler::Ruler;
287 /// let mut chain = Ruler::<&str, fn (&mut String)>::new();
288 ///
289 /// chain.add("a", |s| s.push_str("bar"));
290 /// chain.add("b", |s| s.push_str("foo")).before("a");
291 ///
292 /// let mut result = String::new();
293 /// for f in chain.iter() { f(&mut result); }
294 /// assert_eq!(result, "foobar");
295 /// ```
296 pub fn before(&mut self, mark: M) -> &mut Self {
297 self.cons.push(RuleItemConstraint::Before(mark));
298 self
299 }
300
301 /// Make sure this rule will be inserted after any rule defined by `mark` (if such rule exists).
302 /// Similar to [RuleItem::before](RuleItem::before).
303 pub fn after(&mut self, mark: M) -> &mut Self {
304 self.cons.push(RuleItemConstraint::After(mark));
305 self
306 }
307
308 /// This rule will be inserted as early as possible, while still taking into account dependencies,
309 /// i.e. `.after(X).before_all()` causes this to be first rule after X.
310 /// ```
311 /// use markdown_it::common::ruler::Ruler;
312 /// let mut chain = Ruler::<&str, fn (&mut String)>::new();
313 ///
314 /// chain.add("a", |s| s.push_str("A"));
315 /// chain.add("c", |s| s.push_str("C")).after("a");
316 /// chain.add("b", |s| s.push_str("B")).after("a").before_all();
317 ///
318 /// let mut result = String::new();
319 /// for f in chain.iter() { f(&mut result); }
320 /// // without before_all order will be ACB
321 /// assert_eq!(result, "ABC");
322 /// ```
323 pub fn before_all(&mut self) -> &mut Self {
324 self.prio = RuleItemPriority::BeforeAll;
325 self
326 }
327
328 /// This rule will be inserted as late as possible, while still taking into account dependencies,
329 /// i.e. `.before(X).after_all()` causes this to be last rule before X.
330 /// Similar to [RuleItem::before_all](RuleItem::before_all).
331 pub fn after_all(&mut self) -> &mut Self {
332 self.prio = RuleItemPriority::AfterAll;
333 self
334 }
335
336 /// Add another auxiliary identifier to this rule. It can be used to group together multiple
337 /// rules with similar functionality.
338 /// ```
339 /// use markdown_it::common::ruler::Ruler;
340 /// let mut chain = Ruler::<&str, fn (&mut String)>::new();
341 ///
342 /// chain.add("b", |s| s.push_str("B")).alias("BorC");
343 /// chain.add("c", |s| s.push_str("C")).alias("BorC");
344 /// chain.add("a", |s| s.push_str("A")).before("BorC");
345 ///
346 /// let mut result = String::new();
347 /// for f in chain.iter() { f(&mut result); }
348 /// assert_eq!(result, "ABC");
349 /// ```
350 pub fn alias(&mut self, mark: M) -> &mut Self {
351 self.marks.push(mark);
352 self
353 }
354
355 /// Require another rule identified by `mark`, panic if not found.
356 pub fn require(&mut self, mark: M) -> &mut Self {
357 self.cons.push(RuleItemConstraint::Require(mark));
358 self
359 }
360}
361
362#[derive(Debug)]
363enum RuleItemConstraint<M> {
364 Before(M),
365 After(M),
366 Require(M),
367}
368
369#[derive(Debug)]
370enum RuleItemPriority {
371 Normal,
372 BeforeAll,
373 AfterAll,
374}
375
376#[cfg(test)]
377mod tests {
378 use super::Ruler;
379
380 #[test]
381 #[should_panic(expected = r#"cyclic dependency: "A" < "B" < "C" < "D" < "E" < "F" < "A""#)]
382 #[cfg(debug_assertions)]
383 fn cyclic_dependency_debug() {
384 let mut r = Ruler::new();
385 r.add("%", ()).after("D");
386 r.add("A", ()).after("B");
387 r.add("E", ()).after("F");
388 r.add("C", ()).after("D");
389 r.add("B", ()).after("C");
390 r.add("D", ()).after("E");
391 r.add("F", ()).after("A");
392 r.compile();
393 }
394
395 #[test]
396 #[should_panic(expected = r#"cyclic dependency"#)]
397 fn cyclic_dependency() {
398 let mut r = Ruler::new();
399 r.add("A", ()).after("B");
400 r.add("B", ()).after("C");
401 r.add("C", ()).after("A");
402 r.compile();
403 }
404
405 #[test]
406 #[should_panic(expected = r#"missing dependency: "C" requires "Z"#)]
407 fn missing_require() {
408 let mut r = Ruler::new();
409 r.add("A", ());
410 r.add("B", ()).require("A");
411 r.add("C", ()).require("Z");
412 r.compile();
413 }
414}