Skip to main content

mwtitle/
display.rs

1/*
2Copyright (C) 2021 Erutuon
3
4This program is free software: you can redistribute it and/or modify
5it under the terms of the GNU General Public License as published by
6the Free Software Foundation, either version 3 of the License, or
7(at your option) any later version.
8
9This program is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12GNU General Public License for more details.
13
14You should have received a copy of the GNU General Public License
15along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17use std::fmt::Display;
18
19#[derive(Clone, Copy, Debug)]
20#[repr(u8)]
21pub enum TitleWhitespace {
22    Spaces,
23    Underscores,
24}
25
26impl TitleWhitespace {
27    pub(crate) const fn char(self) -> char {
28        match self {
29            TitleWhitespace::Spaces => ' ',
30            TitleWhitespace::Underscores => '_',
31        }
32    }
33
34    pub(crate) const fn other_char(self) -> char {
35        match self {
36            TitleWhitespace::Spaces => '_',
37            TitleWhitespace::Underscores => ' ',
38        }
39    }
40}
41
42/// Used internally to display spaces as underscores and vice-versa.
43/// The only valid values for C are `' '` and `'_'`.
44struct WhitespaceDisplayer<'a>(pub(crate) &'a str, pub(crate) TitleWhitespace);
45
46impl Display for WhitespaceDisplayer<'_> {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        let Self(s, whitespace) = self;
49        use std::fmt::Write as _;
50        // SAFETY: string indexing won't panic
51        // because str::match_indices always returns valid char boundary.
52        let mut last_pos = None;
53        for (pos, _) in s.match_indices(whitespace.other_char()) {
54            f.write_str(&s[last_pos.replace(pos + 1).unwrap_or(0)..pos])?;
55            f.write_char(whitespace.char())?;
56        }
57        if last_pos
58            // If the last whitespace character was not at the end of the string,
59            // write everything after it.
60            .map(|last| last < s.len())
61            // If there were no whitespace characters, write the whole string.
62            .unwrap_or(true)
63        {
64            f.write_str(&s[last_pos.unwrap_or(0)..s.len()])?;
65        }
66        Ok(())
67    }
68}
69
70#[test]
71fn whitespace_displayer_displays_spaces_as_underscores() {
72    for (input, expected) in [
73        (" ", "_"),
74        ("   ", "___"),
75        ("a b", "a_b"),
76        (" a", "_a"),
77        ("a ", "a_"),
78        (" a b ", "_a_b_"),
79        ("a  b", "a__b"),
80        ("  ab", "__ab"),
81        ("ab  ", "ab__"),
82        ("  a  b", "__a__b"),
83        ("  a  b  ", "__a__b__"),
84    ] {
85        for input in [input, &input.replace(' ', "_")] {
86            assert_eq!(
87                &WhitespaceDisplayer(input, TitleWhitespace::Underscores)
88                    .to_string(),
89                expected,
90                "\n{input:?}"
91            );
92        }
93    }
94}
95
96pub(crate) struct TitleDisplay<'a> {
97    pub(crate) interwiki: Option<&'a str>,
98    pub(crate) namespace: Option<&'a str>,
99    pub(crate) dbkey: &'a str,
100    pub(crate) fragment: Option<&'a str>,
101    pub(crate) whitespace: TitleWhitespace,
102}
103
104impl Display for TitleDisplay<'_> {
105    fn fmt(
106        &self,
107        f: &mut std::fmt::Formatter<'_>,
108    ) -> std::result::Result<(), std::fmt::Error> {
109        let whitespace_displayer = |s| WhitespaceDisplayer(s, self.whitespace);
110        if let Some(interwiki) = self.interwiki {
111            whitespace_displayer(interwiki).fmt(f)?;
112            f.write_str(":")?;
113        }
114        if let Some(namespace) = self.namespace {
115            whitespace_displayer(namespace).fmt(f)?;
116            f.write_str(":")?;
117        }
118        whitespace_displayer(self.dbkey).fmt(f)?;
119        if let Some(fragment) = self.fragment {
120            f.write_str("#")?;
121            fragment.fmt(f)?;
122        }
123        Ok(())
124    }
125}