Skip to main content

links_notation/
syntax.rs

1//! How the references of a parsed document were written.
2//!
3//! The links [`parse_lino_to_links`](crate::parse_lino_to_links) returns hold
4//! what each reference means: `1` and `'1'` are both the reference `1`. The
5//! links [`parse_lino_to_syntax_links`](crate::parse_lino_to_syntax_links)
6//! returns hold a [`SyntaxReference`] instead, which keeps the meaning and also
7//! the text the reference was written as, so a tool can tell the two apart
8//! while comparing, hashing and formatting them exactly as before.
9
10use crate::quotes::{QUOTES, read_reference};
11use std::fmt;
12use std::hash::{Hash, Hasher};
13
14/// A reference read from a document, together with the text it was written as.
15///
16/// Equality, hashing, ordering and [`Display`](fmt::Display) use the
17/// [`value`](Self::value) only, so `1` and `'1'` are equal references and a
18/// `LiNo<SyntaxReference>` formats exactly like the `LiNo<String>` with the
19/// same values. [`raw`](Self::raw) and [`quote`](Self::quote) say how the
20/// reference was written.
21///
22/// # Examples
23/// ```
24/// use links_notation::{LiNo, SyntaxReference, parse_lino_to_syntax_links};
25///
26/// let links = parse_lino_to_syntax_links("(t 1 '1')").unwrap();
27/// let LiNo::Link { values, .. } = &links[0] else { unreachable!() };
28/// let (LiNo::Ref(bare), LiNo::Ref(quoted)) = (&values[1], &values[2]) else {
29///     unreachable!()
30/// };
31/// assert_eq!(bare, quoted);
32/// assert_eq!(bare.raw.as_deref(), Some("1"));
33/// assert_eq!(quoted.raw.as_deref(), Some("'1'"));
34/// assert_eq!((bare.quote(), quoted.quote()), (None, Some('\'')));
35/// assert_eq!(quoted.to_string(), "1");
36/// assert_eq!(SyntaxReference::from("x").raw, None);
37/// ```
38#[derive(Debug, Clone, Default)]
39pub struct SyntaxReference {
40    /// What the reference means: its delimiters and escapes resolved and a
41    /// versioned literal decoded. This is the ID `parse_lino_to_links` reads.
42    pub value: String,
43    /// The reference exactly as the document wrote it: quotes, doubled quote
44    /// runs and `~1{...}` literal syntax included, and the line breaks of a
45    /// multi-line quoted reference. Taken from the document as the caller
46    /// passed it, comments included. `None` for a reference built in code.
47    pub raw: Option<String>,
48}
49
50impl SyntaxReference {
51    /// A reference that was not read from a document, so it has no raw text.
52    pub fn new(value: impl Into<String>) -> Self {
53        Self {
54            value: value.into(),
55            raw: None,
56        }
57    }
58
59    /// A reference with the text it was written as.
60    pub fn with_raw(value: impl Into<String>, raw: impl Into<String>) -> Self {
61        Self {
62            value: value.into(),
63            raw: Some(raw.into()),
64        }
65    }
66
67    /// What the reference means, the same as [`value`](Self::value).
68    pub fn as_str(&self) -> &str {
69        &self.value
70    }
71
72    /// The delimiter the reference was written between: `'`, `"` or `` ` ``
73    /// when it was quoted, and `None` when it was bare, a versioned literal, or
74    /// not read from a document. See [`reference_quote`].
75    pub fn quote(&self) -> Option<char> {
76        self.raw.as_deref().and_then(reference_quote)
77    }
78}
79
80impl PartialEq for SyntaxReference {
81    fn eq(&self, other: &Self) -> bool {
82        self.value == other.value
83    }
84}
85
86impl Eq for SyntaxReference {}
87
88impl Hash for SyntaxReference {
89    fn hash<H: Hasher>(&self, state: &mut H) {
90        self.value.hash(state);
91    }
92}
93
94impl PartialOrd for SyntaxReference {
95    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
96        Some(self.cmp(other))
97    }
98}
99
100impl Ord for SyntaxReference {
101    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
102        self.value.cmp(&other.value)
103    }
104}
105
106impl PartialEq<str> for SyntaxReference {
107    fn eq(&self, other: &str) -> bool {
108        self.value == other
109    }
110}
111
112impl PartialEq<&str> for SyntaxReference {
113    fn eq(&self, other: &&str) -> bool {
114        self.value == *other
115    }
116}
117
118impl PartialEq<String> for SyntaxReference {
119    fn eq(&self, other: &String) -> bool {
120        &self.value == other
121    }
122}
123
124/// Writes the [`value`](SyntaxReference::value), unescaped, the way a `String`
125/// ID is written: a `LiNo` quotes it where the notation needs quotes, so a
126/// `LiNo<SyntaxReference>` prints exactly what the `LiNo<String>` prints.
127impl fmt::Display for SyntaxReference {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.write_str(&self.value)
130    }
131}
132
133impl From<String> for SyntaxReference {
134    fn from(value: String) -> Self {
135        Self::new(value)
136    }
137}
138
139impl From<&str> for SyntaxReference {
140    fn from(value: &str) -> Self {
141        Self::new(value)
142    }
143}
144
145impl From<SyntaxReference> for String {
146    fn from(reference: SyntaxReference) -> Self {
147        reference.value
148    }
149}
150
151/// The delimiter a reference was written between, read from the reference as
152/// it was written: `'`, `"` or `` ` `` for one complete quoted reference
153/// (however many delimiters open it), and `None` for a bare reference, a
154/// versioned literal, or text that holds more than one reference.
155///
156/// # Examples
157/// ```
158/// use links_notation::reference_quote;
159///
160/// assert_eq!(reference_quote("'1'"), Some('\''));
161/// assert_eq!(reference_quote("\"\"a b\"\""), Some('"'));
162/// assert_eq!(reference_quote("`x`"), Some('`'));
163/// assert_eq!(reference_quote("1"), None);
164/// assert_eq!(reference_quote("~1{31}"), None);
165/// // A bare reference that starts with a quote.
166/// assert_eq!(reference_quote("'unclosed"), None);
167/// assert_eq!(reference_quote("'a' b"), None);
168/// assert_eq!(reference_quote(""), None);
169/// ```
170pub fn reference_quote(raw: &str) -> Option<char> {
171    let first = *raw.as_bytes().first()?;
172    if !QUOTES.contains(&first) {
173        return None;
174    }
175    let reading = read_reference(raw)?;
176    (reading.length == raw.len()).then_some(first as char)
177}