acdc_parser/model/
substitution.rs1use serde::{Deserialize, Serialize};
2
3use crate::{AttributeValue, DocumentAttributes};
4
5#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8#[non_exhaustive]
9pub enum Substitution {
10 SpecialChars,
11 Attributes,
12 Replacements,
13 Macros,
14 PostReplacements,
15 Normal,
16 Verbatim,
17 Quotes,
18 Callouts,
19}
20
21impl From<&str> for Substitution {
22 fn from(value: &str) -> Self {
23 match value {
24 "attributes" | "a" => Substitution::Attributes,
25 "replacements" | "r" => Substitution::Replacements,
26 "macros" | "m" => Substitution::Macros,
27 "post_replacements" | "p" => Substitution::PostReplacements,
28 "normal" | "n" => Substitution::Normal,
29 "verbatim" | "v" => Substitution::Verbatim,
30 "quotes" | "q" => Substitution::Quotes,
31 "callouts" => Substitution::Callouts,
32 "specialchars" | "c" | "" => Substitution::SpecialChars, unknown => {
34 tracing::warn!(substitution = %unknown, "unknown substitution type, using SpecialChars as default");
35 Substitution::SpecialChars
36 }
37 }
38 }
39}
40
41#[allow(dead_code)]
42pub const BASIC: &[Substitution] = &[Substitution::SpecialChars];
43pub const HEADER: &[Substitution] = &[Substitution::SpecialChars, Substitution::Attributes];
44pub const NORMAL: &[Substitution] = &[
45 Substitution::SpecialChars,
46 Substitution::Attributes,
47 Substitution::Quotes,
48 Substitution::Replacements,
49 Substitution::Macros,
50 Substitution::PostReplacements,
51];
52#[allow(dead_code)]
53pub const REFTEXT: &[Substitution] = &[
54 Substitution::SpecialChars,
55 Substitution::Quotes,
56 Substitution::Replacements,
57];
58pub const VERBATIM: &[Substitution] = &[Substitution::SpecialChars, Substitution::Callouts];
59
60impl Substitute for &str {}
61impl Substitute for String {}
62
63pub(crate) trait Substitute: ToString {
64 fn substitute(
65 &self,
66 substitutions: &[Substitution],
67 attributes: &DocumentAttributes,
68 ) -> String {
69 let mut text = self.to_string();
70 for substitution in substitutions {
71 match substitution {
72 Substitution::SpecialChars => {
73 text = Self::substitute_special_chars(&text);
74 }
75 Substitution::Attributes => {
76 text = Self::substitute_attributes(&text, attributes);
77 }
78 Substitution::Quotes => {
79 text = Self::substitute_quotes(&text);
80 }
81 Substitution::Replacements => {
82 text = Self::substitute_replacements(&text);
83 }
84 Substitution::Macros => {
85 text = Self::substitute_macros(&text);
86 }
87 Substitution::PostReplacements => {
88 text = Self::substitute_post_replacements(&text);
89 }
90 Substitution::Callouts => {
91 text = Self::substitute_callouts(&text);
92 }
93 Substitution::Normal => {
95 self.substitute(NORMAL, attributes);
96 }
97 Substitution::Verbatim => {
98 self.substitute(VERBATIM, attributes);
99 }
100 }
101 }
102 text
103 }
104
105 #[must_use]
106 fn substitute_special_chars(text: &str) -> String {
107 text.to_string()
108 }
109
110 #[must_use]
116 fn substitute_attributes(text: &str, attributes: &DocumentAttributes) -> String {
117 let mut result = String::with_capacity(text.len());
118 let mut chars = text.chars().peekable();
119
120 while let Some(ch) = chars.next() {
121 if ch == '{' {
122 let mut attr_name = String::new();
124 let mut found_closing_brace = false;
125
126 while let Some(&next_ch) = chars.peek() {
127 if next_ch == '}' {
128 chars.next(); found_closing_brace = true;
130 break;
131 }
132 attr_name.push(next_ch);
133 chars.next();
134 }
135
136 if found_closing_brace {
137 match attributes.get(&attr_name) {
138 Some(AttributeValue::Bool(true)) => {
139 }
141 Some(AttributeValue::String(attr_value)) => {
142 result.push_str(attr_value);
143 }
144 _ => {
145 result.push('{');
147 result.push_str(&attr_name);
148 result.push('}');
149 }
150 }
151 } else {
152 result.push('{');
154 result.push_str(&attr_name);
155 }
156 } else {
157 result.push(ch);
158 }
159 }
160
161 result
162 }
163
164 #[must_use]
165 fn substitute_quotes(text: &str) -> String {
166 text.to_string()
167 }
168
169 #[must_use]
170 fn substitute_replacements(text: &str) -> String {
171 text.to_string()
172 }
173
174 #[must_use]
175 fn substitute_macros(text: &str) -> String {
176 text.to_string()
177 }
178
179 #[must_use]
180 fn substitute_post_replacements(text: &str) -> String {
181 text.to_string()
182 }
183
184 #[must_use]
185 fn substitute_callouts(text: &str) -> String {
186 text.to_string()
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_resolve_attribute_references() {
196 let attribute_weight = AttributeValue::String(String::from("weight"));
198 let attribute_mass = AttributeValue::String(String::from("mass"));
199
200 let attribute_volume_repeat = String::from("value {attribute_volume}");
203
204 let mut attributes = DocumentAttributes::default();
205 attributes.insert("weight".into(), attribute_weight.clone());
206 attributes.insert("mass".into(), attribute_mass.clone());
207
208 let value = "{weight}";
210 let resolved = value.substitute(HEADER, &attributes);
211 assert_eq!(resolved, "weight".to_string());
212
213 let value = "{weight} {mass}";
215 let resolved = value.substitute(HEADER, &attributes);
216 assert_eq!(resolved, "weight mass".to_string());
217
218 let value = "value {attribute_volume}";
220 let resolved = value.substitute(HEADER, &attributes);
221 assert_eq!(resolved, attribute_volume_repeat);
222 }
223
224 #[test]
225 fn test_utf8_boundary_handling() {
226 let attributes = DocumentAttributes::default();
229
230 let value = ":J::~\x01\x00\x00Ô";
232 let resolved = value.substitute(HEADER, &attributes);
233 assert_eq!(resolved, value);
235
236 let value = "{attr}Ô{missing}日本語";
238 let resolved = value.substitute(HEADER, &attributes);
239 assert_eq!(resolved, "{attr}Ô{missing}日本語");
240
241 let value = "{attrÔ}test";
243 let resolved = value.substitute(HEADER, &attributes);
244 assert_eq!(resolved, "{attrÔ}test");
245 }
246}