Skip to main content

molframe_cif/write/
value.rs

1//! One canonical renderer for CIF values and identifiers.
2
3use crate::document::CifValue;
4use std::fmt::{self, Display, Formatter, Write};
5
6/// Renders a typed value as one CIF token or text field.
7#[must_use]
8pub fn render_value(value: &CifValue) -> String {
9    match value {
10        CifValue::Inapplicable => ".".to_owned(),
11        CifValue::Unknown => "?".to_owned(),
12        CifValue::Integer(value) => value.to_string(),
13        CifValue::Float(value) => value.to_string(),
14        CifValue::Text(value) => quote_text(value),
15    }
16}
17
18/// Quotes arbitrary text without changing its value when reparsed.
19#[must_use]
20pub fn quote_text(text: &str) -> String {
21    let mut rendered = String::new();
22    write_quoted(&mut rendered, text);
23    rendered
24}
25
26/// Appends arbitrary text as one CIF token or text field without a temporary allocation.
27///
28/// This is the streaming counterpart of [`quote_text`]. It uses exactly the
29/// same canonical quoting decisions while allowing large columnar writers to
30/// reuse their destination buffer.
31pub fn write_quoted(output: &mut impl Write, text: &str) {
32    let _ = write_quoted_to(output, text);
33}
34
35pub(crate) struct Quoted<'a>(&'a str);
36
37pub(crate) const fn quoted(text: &str) -> Quoted<'_> {
38    Quoted(text)
39}
40
41impl Display for Quoted<'_> {
42    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
43        write_quoted_to(formatter, self.0)
44    }
45}
46
47fn write_quoted_to(output: &mut impl Write, text: &str) -> fmt::Result {
48    if text.contains('\n') || (text.contains('\'') && text.contains('"')) {
49        output.write_str("\n;")?;
50        output.write_str(text)?;
51        return output.write_str("\n;");
52    }
53    if !needs_quoting(text) {
54        return output.write_str(text);
55    }
56    if text.contains('\'') {
57        output.write_char('"')?;
58        output.write_str(text)?;
59        output.write_char('"')
60    } else {
61        output.write_char('\'')?;
62        output.write_str(text)?;
63        output.write_char('\'')
64    }
65}
66
67pub(crate) fn needs_quoting(text: &str) -> bool {
68    text.is_empty()
69        || text.chars().any(char::is_whitespace)
70        || matches!(text, "." | "?")
71        || text.starts_with(['_', '#', '\'', '"', '[', ']', '$', ';'])
72        || ["loop_", "stop_", "global_"]
73            .iter()
74            .any(|keyword| text.eq_ignore_ascii_case(keyword))
75        || text.get(..5).is_some_and(|prefix| {
76            prefix.eq_ignore_ascii_case("data_") || prefix.eq_ignore_ascii_case("save_")
77        })
78}
79
80#[cfg(test)]
81#[path = "value_tests.rs"]
82mod tests;