ratatui_core/symbols/
merge.rs

1//! This module provides strategies for merging symbols in a layout.
2//!
3//! It defines the [`MergeStrategy`] enum, which allows for different behaviors when combining
4//! symbols, such as replacing the previous symbol, merging them if an exact match exists, or using
5//! a fuzzy match to find the closest representation.
6//!
7//! The merging strategies are useful for [collapsing borders] in layouts, where multiple symbols
8//! may need to be combined to create a single, coherent border representation.
9//!
10//! [collapsing borders]: https://ratatui.rs/recipes/layout/collapse-borders
11use core::str::FromStr;
12
13/// A strategy for merging two symbols into one.
14///
15/// This enum defines how two symbols should be merged together, allowing for different behaviors
16/// when combining symbols, such as replacing the previous symbol, merging them if an exact match
17/// exists, or using a fuzzy match to find the closest representation.
18///
19/// This is useful for [collapsing borders] in layouts, where multiple symbols may need to be
20/// combined to create a single, coherent border representation.
21///
22/// Not all combinations of box drawing symbols can be represented as a single unicode character, as
23/// many of them are not defined in the [Box Drawing Unicode block]. This means that some merging
24/// strategies will not yield a valid unicode character. The [`MergeStrategy::Replace`] strategy
25/// will be used as a fallback in such cases, replacing the previous symbol with the next one.
26///
27/// Specifically, the following combinations of box drawing symbols are not defined in the [Box
28/// Drawing Unicode block]:
29///
30/// - Combining any dashed segments with any non dashed segments (e.g. `╎` with `─` or `━`).
31/// - Combining any rounded segments with any other segments (e.g. `╯` with `─` or `━`).
32/// - Combining any double segments with any thick segments (e.g. `═` with `┃` or `━`).
33/// - Combining some double segments with some plain segments (e.g. `┐` with `╔`).
34///
35/// The merging strategies include:
36///
37/// - [`Self::Replace`]: Replaces the previous symbol with the next one.
38/// - [`Self::Exact`]: Merges symbols only if an exact composite unicode character exists, falling
39///   back to [`Self::Replace`] if not.
40/// - [`Self::Fuzzy`]: Merges symbols even if an exact composite unicode character doesn't exist,
41///   using the closest match, and falling back to [`Self::Exact`] if necessary.
42///
43/// See [`Cell::merge_symbol`] for how to use this strategy in practice, and
44/// [`Block::merge_borders`] for a more concrete example of merging borders in a layout.
45///
46/// # Examples
47///
48/// ```
49/// # use ratatui_core::symbols::merge::MergeStrategy;
50///
51/// assert_eq!(MergeStrategy::Replace.merge("│", "━"), "━");
52/// assert_eq!(MergeStrategy::Exact.merge("│", "─"), "┼");
53/// assert_eq!(MergeStrategy::Fuzzy.merge("┘", "╔"), "╬");
54/// ```
55///
56/// [Box Drawing Unicode block]: https://en.wikipedia.org/wiki/Box_Drawing
57/// [collapsing borders]: https://ratatui.rs/recipes/layout/collapse-borders
58/// [`Block::merge_borders`]:
59///     https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html#method.merge_borders
60/// [`Cell::merge_symbol`]: crate::buffer::Cell::merge_symbol
61#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
62pub enum MergeStrategy {
63    /// Replaces the previous symbol with the next one.
64    ///
65    /// This strategy simply replaces the previous symbol with the next one, without attempting to
66    /// merge them. This is useful when you want to ensure that the last rendered symbol takes
67    /// precedence over the previous one, regardless of their compatibility.
68    ///
69    /// The following diagram illustrates how this would apply to several overlapping blocks where
70    /// the thick bordered blocks are rendered last, replacing the previous symbols:
71    ///
72    /// ```text
73    /// ┌───┐    ┌───┐  ┌───┏━━━┓┌───┐
74    /// │   │    │   │  │   ┃   ┃│   │
75    /// │   │    │ ┏━━━┓│   ┃   ┃│   │
76    /// │   │    │ ┃ │ ┃│   ┃   ┃│   │
77    /// └───┏━━━┓└─┃─┘ ┃└───┗━━━┛┏━━━┓
78    ///     ┃   ┃  ┃   ┃         ┃   ┃
79    ///     ┃   ┃  ┗━━━┛         ┃   ┃
80    ///     ┃   ┃                ┃   ┃
81    ///     ┗━━━┛                ┗━━━┛
82    /// ```
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// # use ratatui_core::symbols::merge::MergeStrategy;
88    /// let strategy = MergeStrategy::Replace;
89    /// assert_eq!(strategy.merge("│", "━"), "━");
90    /// ```
91    #[default]
92    Replace,
93
94    /// Merges symbols only if an exact composite unicode character exists.
95    ///
96    /// This strategy attempts to merge two symbols into a single composite unicode character if the
97    /// exact representation exists. If the required unicode symbol does not exist, it falls back to
98    /// [`MergeStrategy::Replace`], replacing the previous symbol with the next one.
99    ///
100    /// The following diagram illustrates how this would apply to several overlapping blocks where
101    /// the thick bordered blocks are rendered last, merging the previous symbols into a single
102    /// composite character. All combinations of the plain and thick segments exist, so these
103    /// symbols can be merged into a single character:
104    ///
105    /// ```text
106    /// ┌───┐    ┌───┐  ┌───┲━━━┓┌───┐
107    /// │   │    │   │  │   ┃   ┃│   │
108    /// │   │    │ ┏━┿━┓│   ┃   ┃│   │
109    /// │   │    │ ┃ │ ┃│   ┃   ┃│   │
110    /// └───╆━━━┓└─╂─┘ ┃└───┺━━━┛┢━━━┪
111    ///     ┃   ┃  ┃   ┃         ┃   ┃
112    ///     ┃   ┃  ┗━━━┛         ┃   ┃
113    ///     ┃   ┃                ┃   ┃
114    ///     ┗━━━┛                ┗━━━┛
115    /// ```
116    ///
117    /// The following diagram illustrates how this would apply to several overlapping blocks where
118    /// the characters don't have a composite unicode character, so the previous symbols are
119    /// replaced by the next one:
120    ///
121    /// ```text
122    /// ┌───┐    ┌───┐  ┌───╔═══╗┌───┐
123    /// │   │    │   │  │   ║   ║│   │
124    /// │   │    │ ╔═╪═╗│   ║   ║│   │
125    /// │   │    │ ║ │ ║│   ║   ║│   │
126    /// └───╔═══╗└─╫─┘ ║└───╚═══╝╔═══╗
127    ///     ║   ║  ║   ║         ║   ║
128    ///     ║   ║  ╚═══╝         ║   ║
129    ///     ║   ║                ║   ║
130    ///     ╚═══╝                ╚═══╝
131    /// ┌───┐    ┌───┐  ┌───╭───╮┌───┐
132    /// │   │    │   │  │   │   ││   │
133    /// │   │    │ ╭─┼─╮│   │   ││   │
134    /// │   │    │ │ │ ││   │   ││   │
135    /// └───╭───╮└─┼─┘ │└───╰───╯╭───╮
136    ///     │   │  │   │         │   │
137    ///     │   │  ╰───╯         │   │
138    ///     │   │                │   │
139    ///     ╰───╯                ╰───╯
140    /// ```
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// # use ratatui_core::symbols::merge::MergeStrategy;
146    /// let strategy = MergeStrategy::Exact;
147    /// assert_eq!(strategy.merge("│", "━"), "┿"); // exact match exists
148    /// assert_eq!(strategy.merge("┘", "╔"), "╔"); // no exact match, falls back to Replace
149    /// ```
150    Exact,
151
152    /// Merges symbols even if an exact composite unicode character doesn't exist, using the closest
153    /// match.
154    ///
155    /// If required unicode symbol exists, acts exactly like [`MergeStrategy::Exact`], if not, the
156    /// following rules are applied:
157    ///
158    /// 1. There are no characters that combine dashed with plain / thick segments, so we replace
159    ///    dashed segments with plain and thick dashed segments with thick. The following diagram
160    ///    shows how this would apply to merging a block with thick dashed borders over a block with
161    ///    plain dashed borders:
162    ///
163    /// ```text
164    /// ┌╌╌╌┐    ┌╌╌╌┐  ┌╌╌╌┲╍╍╍┓┌╌╌╌┐
165    /// ╎   ╎    ╎   ╎  ╎   ╏   ╏╎   ╎
166    /// ╎   ╎    ╎ ┏╍┿╍┓╎   ╏   ╏╎   ╎
167    /// ╎   ╎    ╎ ╏ ╎ ╏╎   ╏   ╏╎   ╎
168    /// └╌╌╌╆╍╍╍┓└╌╂╌┘ ╏└╌╌╌┺╍╍╍┛┢╍╍╍┪
169    ///     ╏   ╏  ╏   ╏         ╏   ╏
170    ///     ╏   ╏  ┗╍╍╍┛         ╏   ╏
171    ///     ╏   ╏                ╏   ╏
172    ///     ┗╍╍╍┛                ┗╍╍╍┛
173    /// ```
174    ///
175    /// 2. There are no characters that combine rounded segments with other segments, so we replace
176    ///    rounded segments with plain. The following diagram shows how this would apply to merging
177    ///    a block with rounded corners over a block with plain corners:
178    ///
179    /// ```text
180    /// ┌───┐    ┌───┐  ┌───┬───╮┌───┐
181    /// │   │    │   │  │   │   ││   │
182    /// │   │    │ ╭─┼─╮│   │   ││   │
183    /// │   │    │ │ │ ││   │   ││   │
184    /// └───┼───╮└─┼─┘ │└───┴───╯├───┤
185    ///     │   │  │   │         │   │
186    ///     │   │  ╰───╯         │   │
187    ///     │   │                │   │
188    ///     ╰───╯                ╰───╯
189    /// ```
190    ///
191    /// 3. There are no symbols that combine thick and double borders, so we replace all double
192    ///    segments with thick or all thick with double. The second symbol parameter takes
193    ///    precedence in choosing whether to use double or thick. The following diagram shows how
194    ///    this would apply to merging a block with double borders over a block with thick borders
195    ///    and then the reverse (merging a block with thick borders over a block with double
196    ///    borders):
197    ///
198    /// ```text
199    /// ┏━━━┓    ┏━━━┓  ┏━━━╦═══╗┏━━━┓
200    /// ┃   ┃    ┃   ┃  ┃   ║   ║┃   ┃
201    /// ┃   ┃    ┃ ╔═╬═╗┃   ║   ║┃   ┃
202    /// ┃   ┃    ┃ ║ ┃ ║┃   ║   ║┃   ┃
203    /// ┗━━━╬═══╗┗━╬━┛ ║┗━━━╩═══╝╠═══╣
204    ///     ║   ║  ║   ║         ║   ║
205    ///     ║   ║  ╚═══╝         ║   ║
206    ///     ║   ║                ║   ║
207    ///     ╚═══╝                ╚═══╝
208    ///
209    /// ╔═══╗    ╔═══╗  ╔═══┳━━━┓╔═══╗
210    /// ║   ║    ║   ║  ║   ┃   ┃║   ║
211    /// ║   ║    ║ ┏━╋━┓║   ┃   ┃║   ║
212    /// ║   ║    ║ ┃ ║ ┃║   ┃   ┃║   ║
213    /// ╚═══╋━━━┓╚═╋═╝ ┃╚═══┻━━━┛┣━━━┫
214    ///     ┃   ┃  ┃   ┃         ┃   ┃
215    ///     ┃   ┃  ┗━━━┛         ┃   ┃
216    ///     ┃   ┃                ┃   ┃
217    ///     ┗━━━┛                ┗━━━┛
218    /// ```
219    ///
220    /// 4. Some combinations of double and plain don't exist, so if the symbol is still
221    ///    unrepresentable, change all plain segments with double or all double with plain. The
222    ///    second symbol parameter takes precedence in choosing whether to use double or plain. The
223    ///    following diagram shows how this would apply to merging a block with double borders over
224    ///    a block with plain borders and then the reverse (merging a block with plain borders over
225    ///    a block with double borders):
226    ///
227    /// ```text
228    /// ┌───┐    ┌───┐  ┌───╦═══╗┌───┐
229    /// │   │    │   │  │   ║   ║│   │
230    /// │   │    │ ╔═╪═╗│   ║   ║│   │
231    /// │   │    │ ║ │ ║│   ║   ║│   │
232    /// └───╬═══╗└─╫─┘ ║└───╩═══╝╠═══╣
233    ///     ║   ║  ║   ║         ║   ║
234    ///     ║   ║  ╚═══╝         ║   ║
235    ///     ║   ║                ║   ║
236    ///     ╚═══╝                ╚═══╝
237    /// ╔═══╗    ╔═══╗  ╔═══┬───┐╔═══╗
238    /// ║   ║    ║   ║  ║   │   │║   ║
239    /// ║   ║    ║ ┌─╫─┐║   │   │║   ║
240    /// ║   ║    ║ │ ║ │║   │   │║   ║
241    /// ╚═══┼───┐╚═╪═╝ │╚═══┴───┘├───┤
242    ///     │   │  │   │         │   │
243    ///     │   │  └───┘         │   │
244    ///     │   │                │   │
245    ///     └───┘                └───┘
246    /// ```
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// # use ratatui_core::symbols::merge::MergeStrategy;
252    /// let strategy = MergeStrategy::Fuzzy;
253    ///
254    /// // exact matches are merged normally
255    /// assert_eq!(strategy.merge("┌", "┐"), "┬");
256    ///
257    /// // dashed segments are replaced with plain
258    /// assert_eq!(strategy.merge("╎", "╍"), "┿");
259    ///
260    /// // rounded segments are replaced with plain
261    /// assert_eq!(strategy.merge("┘", "╭"), "┼");
262    ///
263    /// // double and thick segments are merged based on the second symbol
264    /// assert_eq!(strategy.merge("┃", "═"), "╬");
265    /// assert_eq!(strategy.merge("═", "┃"), "╋");
266    ///
267    /// // combinations of double with plain that don't exist are merged based on the second symbol
268    /// assert_eq!(strategy.merge("┐", "╔"), "╦");
269    /// assert_eq!(strategy.merge("╔", "┐"), "┬");
270    /// ```
271    Fuzzy,
272}
273
274impl MergeStrategy {
275    /// Merges two symbols using this merge strategy.
276    ///
277    /// This method takes two string slices representing the previous and next symbols, and
278    /// returns a string slice representing the merged symbol based on the merge strategy.
279    ///
280    /// If either of the symbols are not in the [Box Drawing Unicode block], the `next` symbol is
281    /// returned as is. If both symbols are valid, they are merged according to the rules defined
282    /// in the [`MergeStrategy`].
283    ///
284    /// Most code using this method will use the [`Cell::merge_symbol`] method, which uses this
285    /// method internally to merge the symbols of a cell.
286    ///
287    /// # Example
288    ///
289    /// ```
290    /// # use ratatui_core::symbols::merge::MergeStrategy;
291    ///
292    /// let strategy = MergeStrategy::Fuzzy;
293    /// assert_eq!(strategy.merge("┌", "┐"), "┬"); // merges to a single character
294    /// assert_eq!(strategy.merge("┘", "╭"), "┼"); // replaces rounded with plain
295    /// assert_eq!(strategy.merge("╎", "╍"), "┿"); // replaces dashed with plain
296    /// assert_eq!(strategy.merge("┐", "╔"), "╦"); // merges double with plain
297    /// assert_eq!(strategy.merge("╔", "┐"), "┬"); // merges plain with double
298    /// ```
299    ///
300    /// [Box Drawing Unicode block]: https://en.wikipedia.org/wiki/Box_Drawing
301    /// [`Cell::merge_symbol`]: crate::buffer::Cell::merge_symbol
302    pub fn merge<'a>(self, prev: &'a str, next: &'a str) -> &'a str {
303        // Replace should always just return the last symbol.
304        if self == Self::Replace {
305            return next;
306        }
307
308        match (BorderSymbol::from_str(prev), BorderSymbol::from_str(next)) {
309            (Ok(prev_symbol), Ok(next_symbol)) => prev_symbol
310                .merge(next_symbol, self)
311                .try_into()
312                .unwrap_or(next),
313            // Non-border symbols take precedence in strategies other than Replace.
314            (Err(_), Ok(_)) => prev,
315            (_, Err(_)) => next,
316        }
317    }
318}
319
320/// Represents a composite border symbol using individual line components.
321///
322/// This is an internal type for now specifically used to make the merge logic easier to implement.
323/// At some point in the future, we might make a similar type public to represent the
324#[derive(Debug, Copy, Clone, PartialEq, Eq)]
325struct BorderSymbol {
326    right: LineStyle,
327    up: LineStyle,
328    left: LineStyle,
329    down: LineStyle,
330}
331
332impl BorderSymbol {
333    /// Creates a new [`BorderSymbol`], based on individual line styles.
334    #[must_use]
335    const fn new(right: LineStyle, up: LineStyle, left: LineStyle, down: LineStyle) -> Self {
336        Self {
337            right,
338            up,
339            left,
340            down,
341        }
342    }
343
344    /// Finds the closest representation of the [`BorderSymbol`], that has a corresponding unicode
345    /// character.
346    #[must_use]
347    fn fuzzy(mut self, other: Self) -> Self {
348        #[allow(clippy::enum_glob_use)]
349        use LineStyle::*;
350
351        // Dashes only include vertical and horizontal lines.
352        if !self.is_straight() {
353            self = self
354                .replace(DoubleDash, Plain)
355                .replace(TripleDash, Plain)
356                .replace(QuadrupleDash, Plain)
357                .replace(DoubleDashThick, Thick)
358                .replace(TripleDashThick, Thick)
359                .replace(QuadrupleDashThick, Thick);
360        }
361
362        // Rounded has only corner variants.
363        if !self.is_corner() {
364            self = self.replace(Rounded, Plain);
365        }
366
367        // There are no Double + Thick variants.
368        if self.contains(Double) && self.contains(Thick) {
369            // Decide whether to use Double or Thick, based on the last merged-in symbol.
370            if other.contains(Double) {
371                self = self.replace(Thick, Double);
372            } else {
373                self = self.replace(Double, Thick);
374            }
375        }
376
377        // Some Plain + Double variants don't exist.
378        if <&str>::try_from(self).is_err() {
379            // Decide whether to use Double or Plain, based on the last merged-in symbol.
380            if other.contains(Double) {
381                self = self.replace(Plain, Double);
382            } else {
383                self = self.replace(Double, Plain);
384            }
385        }
386        self
387    }
388
389    /// Return true only if the symbol is a line and both parts have the same [`LineStyle`].
390    fn is_straight(self) -> bool {
391        use LineStyle::Nothing;
392        (self.up == self.down && self.left == self.right)
393            && (self.up == Nothing || self.left == Nothing)
394    }
395
396    /// Return true only if the symbol is a corner and both parts have the same [`LineStyle`].
397    fn is_corner(self) -> bool {
398        use LineStyle::Nothing;
399        match (self.up, self.right, self.down, self.left) {
400            (up, right, Nothing, Nothing) => up == right,
401            (Nothing, right, down, Nothing) => right == down,
402            (Nothing, Nothing, down, left) => down == left,
403            (up, Nothing, Nothing, left) => up == left,
404            _ => false,
405        }
406    }
407
408    /// Checks if any of the line components making the [`BorderSymbol`] matches the `style`.
409    fn contains(self, style: LineStyle) -> bool {
410        self.up == style || self.right == style || self.down == style || self.left == style
411    }
412
413    /// Replaces all line styles matching `from` by `to`.
414    #[must_use]
415    fn replace(mut self, from: LineStyle, to: LineStyle) -> Self {
416        self.up = if self.up == from { to } else { self.up };
417        self.right = if self.right == from { to } else { self.right };
418        self.down = if self.down == from { to } else { self.down };
419        self.left = if self.left == from { to } else { self.left };
420        self
421    }
422
423    /// Merges two border symbols into one.
424    fn merge(self, other: Self, strategy: MergeStrategy) -> Self {
425        let exact_result = Self::new(
426            self.right.merge(other.right),
427            self.up.merge(other.up),
428            self.left.merge(other.left),
429            self.down.merge(other.down),
430        );
431        match strategy {
432            MergeStrategy::Replace => other,
433            MergeStrategy::Fuzzy => exact_result.fuzzy(other),
434            MergeStrategy::Exact => exact_result,
435        }
436    }
437}
438
439#[derive(thiserror::Error, Debug, PartialEq, Eq)]
440enum BorderSymbolError {
441    #[error("cannot parse &str `{0}` to BorderSymbol")]
442    CannotParse(alloc::string::String),
443    #[error("cannot convert BorderSymbol `{0:#?}` to &str: no such symbol exists")]
444    Unrepresentable(BorderSymbol),
445}
446
447/// A visual style defining the appearance of a single line making up a block border.
448///
449/// This is an internal type used to represent the different styles of lines that can be used in
450/// border symbols.
451///
452/// At some point in the future, we might make this type (or a similar one) public to allow users to
453/// work with line styles directly, but for now, it is used internally only to simplify the merge
454/// logic of border symbols.
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456enum LineStyle {
457    /// Represents the absence of a line.
458    Nothing,
459
460    /// A single line (e.g. `─`, `│`).
461    Plain,
462
463    /// A rounded line style, only applicable in corner symbols (e.g. `╭`, `╯`).
464    Rounded,
465
466    /// A double line (e.g. `═`, `║`).
467    Double,
468
469    /// A thickened line (e.g. `━`, `┃`).
470    Thick,
471
472    /// A dashed line with a double dash pattern (e.g. `╌`, `╎`).
473    DoubleDash,
474
475    /// A thicker variant of the double dash (e.g. `╍`, `╏`)
476    DoubleDashThick,
477
478    /// A dashed line with a triple dash pattern (e.g. `┄`, `┆`).
479    TripleDash,
480
481    /// A thicker variant of the triple dash (e.g. `┅`, `┇`).
482    TripleDashThick,
483
484    /// A dashed line with four dashes (e.g. `┈`, `┊`).
485    QuadrupleDash,
486
487    /// A thicker variant of the quadruple dash (e.g. `┉`, `┋`).
488    QuadrupleDashThick,
489}
490
491impl LineStyle {
492    /// Merges line styles.
493    #[must_use]
494    pub fn merge(self, other: Self) -> Self {
495        if other == Self::Nothing { self } else { other }
496    }
497}
498
499// Defines a translation between `BorderSymbol` and the corresponding character.
500macro_rules! define_symbols {
501    (
502        $( $symbol:expr => ($right:ident, $up:ident, $left:ident, $down:ident) ),* $(,)?
503    ) => {
504
505        impl FromStr for BorderSymbol {
506            type Err = BorderSymbolError;
507            fn from_str(s: &str) -> Result<Self, Self::Err> {
508                use LineStyle::*;
509                use alloc::string::ToString;
510                match s {
511                    $( $symbol => Ok(Self::new($right, $up, $left, $down)) ),* ,
512                    _ => Err(BorderSymbolError::CannotParse(s.to_string())),
513                }
514            }
515        }
516
517        impl TryFrom<BorderSymbol> for &'static str {
518            type Error = BorderSymbolError;
519            fn try_from(value: BorderSymbol) -> Result<Self, Self::Error> {
520                use LineStyle::*;
521                match (value.right, value.up, value.left, value.down) {
522                    $( ($right, $up, $left, $down) => Ok($symbol) ),* ,
523                    _ => Err(BorderSymbolError::Unrepresentable(value)),
524                }
525            }
526        }
527    };
528}
529
530define_symbols!(
531    "─" => (Plain, Nothing, Plain, Nothing),
532    "━" => (Thick, Nothing, Thick, Nothing),
533    "│" => (Nothing, Plain, Nothing, Plain),
534    "┃" => (Nothing, Thick, Nothing, Thick),
535    "┄" => (TripleDash, Nothing, TripleDash, Nothing),
536    "┅" => (TripleDashThick, Nothing, TripleDashThick, Nothing),
537    "┆" => (Nothing, TripleDash, Nothing, TripleDash),
538    "┇" => (Nothing, TripleDashThick, Nothing, TripleDashThick),
539    "┈" => (QuadrupleDash, Nothing, QuadrupleDash, Nothing),
540    "┉" => (QuadrupleDashThick, Nothing, QuadrupleDashThick, Nothing),
541    "┊" => (Nothing, QuadrupleDash, Nothing, QuadrupleDash),
542    "┋" => (Nothing, QuadrupleDashThick, Nothing, QuadrupleDashThick),
543    "┌" => (Plain, Nothing, Nothing, Plain),
544    "┍" => (Thick, Nothing, Nothing, Plain),
545    "┎" => (Plain, Nothing, Nothing, Thick),
546    "┏" => (Thick, Nothing, Nothing, Thick),
547    "┐" => (Nothing, Nothing, Plain, Plain),
548    "┑" => (Nothing, Nothing, Thick, Plain),
549    "┒" => (Nothing, Nothing, Plain, Thick),
550    "┓" => (Nothing, Nothing, Thick, Thick),
551    "└" => (Plain, Plain, Nothing, Nothing),
552    "┕" => (Thick, Plain, Nothing, Nothing),
553    "┖" => (Plain, Thick, Nothing, Nothing),
554    "┗" => (Thick, Thick, Nothing, Nothing),
555    "┘" => (Nothing, Plain, Plain, Nothing),
556    "┙" => (Nothing, Plain, Thick, Nothing),
557    "┚" => (Nothing, Thick, Plain, Nothing),
558    "┛" => (Nothing, Thick, Thick, Nothing),
559    "├" => (Plain, Plain, Nothing, Plain),
560    "┝" => (Thick, Plain, Nothing, Plain),
561    "┞" => (Plain, Thick, Nothing, Plain),
562    "┟" => (Plain, Plain, Nothing, Thick),
563    "┠" => (Plain, Thick, Nothing, Thick),
564    "┡" => (Thick, Thick, Nothing, Plain),
565    "┢" => (Thick, Plain, Nothing, Thick),
566    "┣" => (Thick, Thick, Nothing, Thick),
567    "┤" => (Nothing, Plain, Plain, Plain),
568    "┥" => (Nothing, Plain, Thick, Plain),
569    "┦" => (Nothing, Thick, Plain, Plain),
570    "┧" => (Nothing, Plain, Plain, Thick),
571    "┨" => (Nothing, Thick, Plain, Thick),
572    "┩" => (Nothing, Thick, Thick, Plain),
573    "┪" => (Nothing, Plain, Thick, Thick),
574    "┫" => (Nothing, Thick, Thick, Thick),
575    "┬" => (Plain, Nothing, Plain, Plain),
576    "┭" => (Plain, Nothing, Thick, Plain),
577    "┮" => (Thick, Nothing, Plain, Plain),
578    "┯" => (Thick, Nothing, Thick, Plain),
579    "┰" => (Plain, Nothing, Plain, Thick),
580    "┱" => (Plain, Nothing, Thick, Thick),
581    "┲" => (Thick, Nothing, Plain, Thick),
582    "┳" => (Thick, Nothing, Thick, Thick),
583    "┴" => (Plain, Plain, Plain, Nothing),
584    "┵" => (Plain, Plain, Thick, Nothing),
585    "┶" => (Thick, Plain, Plain, Nothing),
586    "┷" => (Thick, Plain, Thick, Nothing),
587    "┸" => (Plain, Thick, Plain, Nothing),
588    "┹" => (Plain, Thick, Thick, Nothing),
589    "┺" => (Thick, Thick, Plain, Nothing),
590    "┻" => (Thick, Thick, Thick, Nothing),
591    "┼" => (Plain, Plain, Plain, Plain),
592    "┽" => (Plain, Plain, Thick, Plain),
593    "┾" => (Thick, Plain, Plain, Plain),
594    "┿" => (Thick, Plain, Thick, Plain),
595    "╀" => (Plain, Thick, Plain, Plain),
596    "╁" => (Plain, Plain, Plain, Thick),
597    "╂" => (Plain, Thick, Plain, Thick),
598    "╃" => (Plain, Thick, Thick, Plain),
599    "╄" => (Thick, Thick, Plain, Plain),
600    "╅" => (Plain, Plain, Thick, Thick),
601    "╆" => (Thick, Plain, Plain, Thick),
602    "╇" => (Thick, Thick, Thick, Plain),
603    "╈" => (Thick, Plain, Thick, Thick),
604    "╉" => (Plain, Thick, Thick, Thick),
605    "╊" => (Thick, Thick, Plain, Thick),
606    "╋" => (Thick, Thick, Thick, Thick),
607    "╌" => (DoubleDash, Nothing, DoubleDash, Nothing),
608    "╍" => (DoubleDashThick, Nothing, DoubleDashThick, Nothing),
609    "╎" => (Nothing, DoubleDash, Nothing, DoubleDash),
610    "╏" => (Nothing, DoubleDashThick, Nothing, DoubleDashThick),
611    "═" => (Double, Nothing, Double, Nothing),
612    "║" => (Nothing, Double, Nothing, Double),
613    "╒" => (Double, Nothing, Nothing, Plain),
614    "╓" => (Plain, Nothing, Nothing, Double),
615    "╔" => (Double, Nothing, Nothing, Double),
616    "╕" => (Nothing, Nothing, Double, Plain),
617    "╖" => (Nothing, Nothing, Plain, Double),
618    "╗" => (Nothing, Nothing, Double, Double),
619    "╘" => (Double, Plain, Nothing, Nothing),
620    "╙" => (Plain, Double, Nothing, Nothing),
621    "╚" => (Double, Double, Nothing, Nothing),
622    "╛" => (Nothing, Plain, Double, Nothing),
623    "╜" => (Nothing, Double, Plain, Nothing),
624    "╝" => (Nothing, Double, Double, Nothing),
625    "╞" => (Double, Plain, Nothing, Plain),
626    "╟" => (Plain, Double, Nothing, Double),
627    "╠" => (Double, Double, Nothing, Double),
628    "╡" => (Nothing, Plain, Double, Plain),
629    "╢" => (Nothing, Double, Plain, Double),
630    "╣" => (Nothing, Double, Double, Double),
631    "╤" => (Double, Nothing, Double, Plain),
632    "╥" => (Plain, Nothing, Plain, Double),
633    "╦" => (Double, Nothing, Double, Double),
634    "╧" => (Double, Plain, Double, Nothing),
635    "╨" => (Plain, Double, Plain, Nothing),
636    "╩" => (Double, Double, Double, Nothing),
637    "╪" => (Double, Plain, Double, Plain),
638    "╫" => (Plain, Double, Plain, Double),
639    "╬" => (Double, Double, Double, Double),
640    "╭" => (Rounded, Nothing, Nothing, Rounded),
641    "╮" => (Nothing, Nothing, Rounded, Rounded),
642    "╯" => (Nothing, Rounded, Rounded, Nothing),
643    "╰" => (Rounded, Rounded, Nothing, Nothing),
644    "╴" => (Nothing, Nothing, Plain, Nothing),
645    "╵" => (Nothing, Plain, Nothing, Nothing),
646    "╶" => (Plain, Nothing, Nothing, Nothing),
647    "╷" => (Nothing, Nothing, Nothing, Plain),
648    "╸" => (Nothing, Nothing, Thick, Nothing),
649    "╹" => (Nothing, Thick, Nothing, Nothing),
650    "╺" => (Thick, Nothing, Nothing, Nothing),
651    "╻" => (Nothing, Nothing, Nothing, Thick),
652    "╼" => (Thick, Nothing, Plain, Nothing),
653    "╽" => (Nothing, Plain, Nothing, Thick),
654    "╾" => (Plain, Nothing, Thick, Nothing),
655    "╿" => (Nothing, Thick, Nothing, Plain),
656);
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    #[test]
663    fn replace_merge_strategy() {
664        let strategy = MergeStrategy::Replace;
665        let symbols = [
666            "─", "━", "│", "┃", "┄", "┅", "┆", "┇", "┈", "┉", "┊", "┋", "┌", "┍", "┎", "┏", "┐",
667            "┑", "┒", "┓", "└", "┕", "┖", "┗", "┘", "┙", "┚", "┛", "├", "┝", "┞", "┟", "┠", "┡",
668            "┢", "┣", "┤", "┥", "┦", "┧", "┨", "┩", "┪", "┫", "┬", "┭", "┮", "┯", "┰", "┱", "┲",
669            "┳", "┴", "┵", "┶", "┷", "┸", "┹", "┺", "┻", "┼", "┽", "┾", "┿", "╀", "╁", "╂", "╃",
670            "╄", "╅", "╆", "╇", "╈", "╉", "╊", "╋", "╌", "╍", "╎", "╏", "═", "║", "╒", "╓", "╔",
671            "╕", "╖", "╗", "╘", "╙", "╚", "╛", "╜", "╝", "╞", "╟", "╠", "╡", "╢", "╣", "╤", "╥",
672            "╦", "╧", "╨", "╩", "╪", "╫", "╬", "╭", "╮", "╯", "╰", "╴", "╵", "╶", "╷", "╸", "╹",
673            "╺", "╻", "╼", "╽", "╾", "╿", " ", "a", "b",
674        ];
675
676        for a in symbols {
677            for b in symbols {
678                assert_eq!(strategy.merge(a, b), b);
679            }
680        }
681    }
682
683    #[test]
684    fn exact_merge_strategy() {
685        let strategy = MergeStrategy::Exact;
686        assert_eq!(strategy.merge("┆", "─"), "─");
687        assert_eq!(strategy.merge("┏", "┆"), "┆");
688        assert_eq!(strategy.merge("╎", "┉"), "┉");
689        assert_eq!(strategy.merge("╎", "┉"), "┉");
690        assert_eq!(strategy.merge("┋", "┋"), "┋");
691        assert_eq!(strategy.merge("╷", "╶"), "┌");
692        assert_eq!(strategy.merge("╭", "┌"), "┌");
693        assert_eq!(strategy.merge("│", "┕"), "┝");
694        assert_eq!(strategy.merge("┏", "│"), "┝");
695        assert_eq!(strategy.merge("│", "┏"), "┢");
696        assert_eq!(strategy.merge("╽", "┕"), "┢");
697        assert_eq!(strategy.merge("│", "─"), "┼");
698        assert_eq!(strategy.merge("┘", "┌"), "┼");
699        assert_eq!(strategy.merge("┵", "┝"), "┿");
700        assert_eq!(strategy.merge("│", "━"), "┿");
701        assert_eq!(strategy.merge("┵", "╞"), "╞");
702        assert_eq!(strategy.merge(" ", "╠"), " ");
703        assert_eq!(strategy.merge("╠", " "), " ");
704        assert_eq!(strategy.merge("╎", "╧"), "╧");
705        assert_eq!(strategy.merge("╛", "╒"), "╪");
706        assert_eq!(strategy.merge("│", "═"), "╪");
707        assert_eq!(strategy.merge("╤", "╧"), "╪");
708        assert_eq!(strategy.merge("╡", "╞"), "╪");
709        assert_eq!(strategy.merge("┌", "╭"), "╭");
710        assert_eq!(strategy.merge("┘", "╭"), "╭");
711        assert_eq!(strategy.merge("┌", "a"), "a");
712        assert_eq!(strategy.merge("a", "╭"), "a");
713        assert_eq!(strategy.merge("a", "b"), "b");
714    }
715
716    #[test]
717    fn fuzzy_merge_strategy() {
718        let strategy = MergeStrategy::Fuzzy;
719        assert_eq!(strategy.merge("┄", "╴"), "─");
720        assert_eq!(strategy.merge("│", "┆"), "┆");
721        assert_eq!(strategy.merge(" ", "┉"), " ");
722        assert_eq!(strategy.merge("┋", "┋"), "┋");
723        assert_eq!(strategy.merge("╷", "╶"), "┌");
724        assert_eq!(strategy.merge("╭", "┌"), "┌");
725        assert_eq!(strategy.merge("│", "┕"), "┝");
726        assert_eq!(strategy.merge("┏", "│"), "┝");
727        assert_eq!(strategy.merge("┏", "┆"), "┝");
728        assert_eq!(strategy.merge("│", "┏"), "┢");
729        assert_eq!(strategy.merge("╽", "┕"), "┢");
730        assert_eq!(strategy.merge("│", "─"), "┼");
731        assert_eq!(strategy.merge("┆", "─"), "┼");
732        assert_eq!(strategy.merge("┘", "┌"), "┼");
733        assert_eq!(strategy.merge("┘", "╭"), "┼");
734        assert_eq!(strategy.merge("╎", "┉"), "┿");
735        assert_eq!(strategy.merge(" ", "╠"), " ");
736        assert_eq!(strategy.merge("╠", " "), " ");
737        assert_eq!(strategy.merge("┵", "╞"), "╪");
738        assert_eq!(strategy.merge("╛", "╒"), "╪");
739        assert_eq!(strategy.merge("│", "═"), "╪");
740        assert_eq!(strategy.merge("╤", "╧"), "╪");
741        assert_eq!(strategy.merge("╡", "╞"), "╪");
742        assert_eq!(strategy.merge("╎", "╧"), "╪");
743        assert_eq!(strategy.merge("┌", "╭"), "╭");
744        assert_eq!(strategy.merge("┌", "a"), "a");
745        assert_eq!(strategy.merge("a", "╭"), "a");
746        assert_eq!(strategy.merge("a", "b"), "b");
747    }
748}