Skip to main content

markdown/
marks.rs

1//! The inline marks an app spells itself.
2//!
3//! [`Mark`](crate::Mark) is closed — bold, italic, strike, code, links — because
4//! every one of those has a spelling CommonMark already reads. Underline,
5//! highlight and a colour do not, so they cannot be variants here without this
6//! crate inventing markdown for everyone. An app that wants them registers a
7//! name and the delimiter that spells it, and the parse and the serializer take
8//! it from there:
9//!
10//! ```
11//! let marks = markdown::Marks::new().with("highlight", "==");
12//! let doc = markdown::parse_with("a ==lit== word", &marks);
13//! assert_eq!(markdown::serialize_with(&doc, &marks), "a ==lit== word");
14//! ```
15//!
16//! The registry is a *parameter* rather than a global because [`parse_with`]
17//! and [`serialize_with`] are pure — the same reason the highlighter is a
18//! function pointer rather than a dependency. [`crate::AppExt::set_marks`] is the gpui-side
19//! half, for the editing surface, which has a `cx` and no other way to know.
20//!
21//! A delimiter markdown already spells (`*`, `_`, `` ` ``, `~`, `[`) is yours to
22//! avoid: CommonMark reads it first and the registration never fires.
23//!
24//! [`parse_with`]: crate::parse_with
25//! [`serialize_with`]: crate::serialize_with
26
27use gpui_shared_string::SharedString;
28
29/// The custom marks a document is read and written with.
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
31pub struct Marks {
32    entries: Vec<Entry>,
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub(crate) struct Entry {
37    pub(crate) name: SharedString,
38    pub(crate) delimiter: SharedString,
39}
40
41impl Marks {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Register `name`, spelled by `delimiter` on both sides — `==lit==`.
47    ///
48    /// An empty delimiter, or a second registration of a name or a delimiter
49    /// already taken, is ignored: a registry that can hold two answers for one
50    /// spelling has no reading of a document to offer.
51    ///
52    /// A registered mark opens and closes on one line, and is plain text
53    /// inside a fence, a code span, raw HTML, a link's destination or a
54    /// picture's caption.
55    pub fn with(
56        mut self,
57        name: impl Into<SharedString>,
58        delimiter: impl Into<SharedString>,
59    ) -> Self {
60        let (name, delimiter) = (name.into(), delimiter.into());
61        let taken = self
62            .entries
63            .iter()
64            .any(|entry| entry.name == name || entry.delimiter == delimiter);
65        if !delimiter.is_empty() && !taken {
66            self.entries.push(Entry { name, delimiter });
67        }
68        self
69    }
70
71    pub fn is_empty(&self) -> bool {
72        self.entries.is_empty()
73    }
74
75    /// Every registered name, in the order they were added.
76    pub fn names(&self) -> impl Iterator<Item = &SharedString> {
77        self.entries.iter().map(|entry| &entry.name)
78    }
79
80    /// What spells `name`, for a caller writing its own markdown.
81    pub fn delimiter(&self, name: &str) -> Option<&str> {
82        self.entries
83            .iter()
84            .find(|entry| entry.name == name)
85            .map(|entry| entry.delimiter.as_ref())
86    }
87
88    /// The entries, longest delimiter first — `===` has to be tried before `==`
89    /// or it is never reached.
90    pub(crate) fn sorted(&self) -> Vec<&Entry> {
91        let mut entries: Vec<&Entry> = self.entries.iter().collect();
92        entries.sort_by_key(|entry| std::cmp::Reverse(entry.delimiter.len()));
93        entries
94    }
95
96    pub(crate) fn index(&self, ix: usize) -> Option<&Entry> {
97        self.entries.get(ix)
98    }
99
100    pub(crate) fn position(&self, entry: &Entry) -> Option<usize> {
101        self.entries.iter().position(|row| row == entry)
102    }
103}