1use crate::{2 error::{Error::*, Result},3 throw, State, Val,4};56#[derive(PartialEq, Eq, Clone, Copy)]7pub enum ManifestType {8 9 Manifest,10 11 12 Std,13 14 ToString,15 16 Minify,17}1819pub struct ManifestJsonOptions<'s> {20 pub padding: &'s str,21 pub mtype: ManifestType,22 pub newline: &'s str,23 pub key_val_sep: &'s str,24 #[cfg(feature = "exp-preserve-order")]25 pub preserve_order: bool,26}2728pub fn manifest_json_ex(s: State, val: &Val, options: &ManifestJsonOptions<'_>) -> Result<String> {29 let mut out = String::new();30 manifest_json_ex_buf(s, val, &mut out, &mut String::new(), options)?;31 Ok(out)32}33fn manifest_json_ex_buf(34 s: State,35 val: &Val,36 buf: &mut String,37 cur_padding: &mut String,38 options: &ManifestJsonOptions<'_>,39) -> Result<()> {40 use std::fmt::Write;41 let mtype = options.mtype;42 match val {43 Val::Bool(v) => {44 if *v {45 buf.push_str("true");46 } else {47 buf.push_str("false");48 }49 }50 Val::Null => buf.push_str("null"),51 Val::Str(s) => escape_string_json_buf(s, buf),52 Val::Num(n) => write!(buf, "{}", n).unwrap(),53 Val::Arr(items) => {54 buf.push('[');55 if !items.is_empty() {56 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {57 buf.push_str(options.newline);58 }5960 let old_len = cur_padding.len();61 cur_padding.push_str(options.padding);62 for (i, item) in items.iter(s.clone()).enumerate() {63 if i != 0 {64 buf.push(',');65 if mtype == ManifestType::ToString {66 buf.push(' ');67 } else if mtype != ManifestType::Minify {68 buf.push_str(options.newline);69 }70 }71 buf.push_str(cur_padding);72 manifest_json_ex_buf(s.clone(), &item?, buf, cur_padding, options)?;73 }74 cur_padding.truncate(old_len);7576 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {77 buf.push_str(options.newline);78 buf.push_str(cur_padding);79 }80 } else if mtype == ManifestType::Std {81 buf.push_str("\n\n");82 buf.push_str(cur_padding);83 } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest {84 buf.push(' ');85 }86 buf.push(']');87 }88 Val::Obj(obj) => {89 obj.run_assertions(s.clone())?;90 buf.push('{');91 let fields = obj.fields(92 #[cfg(feature = "exp-preserve-order")]93 options.preserve_order,94 );95 if !fields.is_empty() {96 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {97 buf.push_str(options.newline);98 }99100 let old_len = cur_padding.len();101 cur_padding.push_str(options.padding);102 for (i, field) in fields.into_iter().enumerate() {103 if i != 0 {104 buf.push(',');105 if mtype == ManifestType::ToString {106 buf.push(' ');107 } else if mtype != ManifestType::Minify {108 buf.push_str(options.newline);109 }110 }111 buf.push_str(cur_padding);112 escape_string_json_buf(&field, buf);113 buf.push_str(options.key_val_sep);114 s.push_description(115 || format!("field <{}> manifestification", field.clone()),116 || {117 let value = obj.get(s.clone(), field.clone())?.unwrap();118 manifest_json_ex_buf(s.clone(), &value, buf, cur_padding, options)?;119 Ok(Val::Null)120 },121 )?;122 }123 cur_padding.truncate(old_len);124125 if mtype != ManifestType::ToString && mtype != ManifestType::Minify {126 buf.push_str(options.newline);127 buf.push_str(cur_padding);128 }129 } else if mtype == ManifestType::Std {130 buf.push_str("\n\n");131 buf.push_str(cur_padding);132 } else if mtype == ManifestType::ToString || mtype == ManifestType::Manifest {133 buf.push(' ');134 }135 buf.push('}');136 }137 Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())),138 };139 Ok(())140}141142pub fn escape_string_json(s: &str) -> String {143 let mut buf = String::new();144 escape_string_json_buf(s, &mut buf);145 buf146}147148fn escape_string_json_buf(s: &str, buf: &mut String) {149 use std::fmt::Write;150 buf.push('"');151 for c in s.chars() {152 match c {153 '"' => buf.push_str("\\\""),154 '\\' => buf.push_str("\\\\"),155 '\u{0008}' => buf.push_str("\\b"),156 '\u{000c}' => buf.push_str("\\f"),157 '\n' => buf.push_str("\\n"),158 '\r' => buf.push_str("\\r"),159 '\t' => buf.push_str("\\t"),160 c if c < 32 as char || (c >= 127 as char && c <= 159 as char) => {161 write!(buf, "\\u{:04x}", c as u32).unwrap();162 }163 c => buf.push(c),164 }165 }166 buf.push('"');167}168169pub struct ManifestYamlOptions<'s> {170 171 172 173 174 175 176 pub padding: &'s str,177 178 179 180 181 182 183 pub arr_element_padding: &'s str,184 185 186 187 188 189 190 pub quote_keys: bool,191 192 193 #[cfg(feature = "exp-preserve-order")]194 pub preserve_order: bool,195}196197198199fn yaml_needs_quotes(string: &str) -> bool {200 fn need_quotes_spaces(string: &str) -> bool {201 string.starts_with(' ') || string.ends_with(' ')202 }203204 string.is_empty()205 || need_quotes_spaces(string)206 || string.starts_with(|c| matches!(c, '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@'))207 || string.contains(|c| matches!(c, ':' | '{' | '}' | '[' | ']' | ',' | '#' | '`' | '\"' | '\'' | '\\' | '\0'..='\x06' | '\t' | '\n' | '\r' | '\x0e'..='\x1a' | '\x1c'..='\x1f'))208 || [209 210 211 212 213 "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", "false",214 "on", "On", "ON", "off", "Off", "OFF", 215 "null", "Null", "NULL", "~",216 ].contains(&string)217 || (string.chars().all(|c| matches!(c, '0'..='9' | '-'))218 && string.chars().filter(|c| *c == '-').count() == 2)219 || string.starts_with('.')220 || string.starts_with("0x")221 || string.parse::<i64>().is_ok()222 || string.parse::<f64>().is_ok()223}224225pub fn manifest_yaml_ex(s: State, val: &Val, options: &ManifestYamlOptions<'_>) -> Result<String> {226 let mut out = String::new();227 manifest_yaml_ex_buf(s, val, &mut out, &mut String::new(), options)?;228 Ok(out)229}230231#[allow(clippy::too_many_lines)]232fn manifest_yaml_ex_buf(233 s: State,234 val: &Val,235 buf: &mut String,236 cur_padding: &mut String,237 options: &ManifestYamlOptions<'_>,238) -> Result<()> {239 use std::fmt::Write;240 match val {241 Val::Bool(v) => {242 if *v {243 buf.push_str("true");244 } else {245 buf.push_str("false");246 }247 }248 Val::Null => buf.push_str("null"),249 Val::Str(s) => {250 if s.is_empty() {251 buf.push_str("\"\"");252 } else if let Some(s) = s.strip_suffix('\n') {253 buf.push('|');254 for line in s.split('\n') {255 buf.push('\n');256 buf.push_str(cur_padding);257 buf.push_str(options.padding);258 buf.push_str(line);259 }260 } else if !options.quote_keys && !yaml_needs_quotes(s) {261 buf.push_str(s);262 } else {263 escape_string_json_buf(s, buf);264 }265 }266 Val::Num(n) => write!(buf, "{}", *n).unwrap(),267 Val::Arr(a) => {268 if a.is_empty() {269 buf.push_str("[]");270 } else {271 for (i, item) in a.iter(s.clone()).enumerate() {272 if i != 0 {273 buf.push('\n');274 buf.push_str(cur_padding);275 }276 let item = item?;277 buf.push('-');278 match &item {279 Val::Arr(a) if !a.is_empty() => {280 buf.push('\n');281 buf.push_str(cur_padding);282 buf.push_str(options.padding);283 }284 _ => buf.push(' '),285 }286 let extra_padding = match &item {287 Val::Arr(a) => !a.is_empty(),288 Val::Obj(o) => !o.is_empty(),289 _ => false,290 };291 let prev_len = cur_padding.len();292 if extra_padding {293 cur_padding.push_str(options.padding);294 }295 manifest_yaml_ex_buf(s.clone(), &item, buf, cur_padding, options)?;296 cur_padding.truncate(prev_len);297 }298 }299 }300 Val::Obj(o) => {301 if o.is_empty() {302 buf.push_str("{}");303 } else {304 for (i, key) in o305 .fields(306 #[cfg(feature = "exp-preserve-order")]307 options.preserve_order,308 )309 .iter()310 .enumerate()311 {312 if i != 0 {313 buf.push('\n');314 buf.push_str(cur_padding);315 }316 if !options.quote_keys && !yaml_needs_quotes(key) {317 buf.push_str(key);318 } else {319 escape_string_json_buf(key, buf);320 }321 buf.push(':');322 let prev_len = cur_padding.len();323 let item = o.get(s.clone(), key.clone())?.expect("field exists");324 match &item {325 Val::Arr(a) if !a.is_empty() => {326 buf.push('\n');327 buf.push_str(cur_padding);328 buf.push_str(options.arr_element_padding);329 cur_padding.push_str(options.arr_element_padding);330 }331 Val::Obj(o) if !o.is_empty() => {332 buf.push('\n');333 buf.push_str(cur_padding);334 buf.push_str(options.padding);335 cur_padding.push_str(options.padding);336 }337 _ => buf.push(' '),338 }339 manifest_yaml_ex_buf(s.clone(), &item, buf, cur_padding, options)?;340 cur_padding.truncate(prev_len);341 }342 }343 }344 Val::Func(_) => throw!(RuntimeError("tried to manifest function".into())),345 }346 Ok(())347}