use oneyaml_core::{Emit, Limits, QuoteStyle, Settings, code};
use super::{EditError, ScalarStyle};
use crate::emit::scalar::{self, BlockIndent, Rules, Style};
use crate::emit::{Start, emit_document, output_rules};
use crate::value::{Mapping, MappingIter, Value};
pub(super) struct Render {
rules: Rules,
limits: Limits,
double: bool,
nl: &'static str,
one_line: bool,
}
enum Task<'a> {
Node(&'a Value),
Key(&'a Value),
Text(&'static str),
Seq(std::slice::Iter<'a, Value>, bool),
Map(MappingIter<'a>, bool),
}
fn unrepresentable(what: &str) -> EditError {
EditError::rejected(&crate::Error::new(
code::UNREPRESENTABLE_VALUE,
what.to_owned(),
None,
))
}
const MAX_IMPLICIT_KEY: usize = 1024;
impl Render {
pub(super) fn new(
settings: &Settings,
emit: &Emit,
nl: &'static str,
) -> Result<Self, EditError> {
output_rules(settings).map_err(|e| EditError::rejected(&e))?;
let rules = Rules::new(emit, settings).map_err(|e| EditError::rejected(&e))?;
Ok(Self {
rules,
limits: settings.get_limits().clone(),
double: emit.get_quote_style() == QuoteStyle::Double,
nl,
one_line: false,
})
}
pub(super) fn one_line(mut self) -> Self {
self.one_line = true;
self
}
fn flat(&self, v: &Value) -> bool {
self.one_line && multiline(v)
}
pub(super) fn root(&self, v: &Value) -> Result<String, EditError> {
if self.flat(v) {
return self.flow(v);
}
self.emit_root(v)
}
fn emit_root(&self, v: &Value) -> Result<String, EditError> {
let mut out = String::new();
emit_document(&mut out, v, &self.rules, &self.limits, Start::First)
.map_err(|e| EditError::rejected(&e))?;
if out.ends_with('\n') && !out.ends_with("\n\n") {
out.pop();
}
if self.nl != "\n" {
out = out.replace('\n', self.nl);
}
Ok(out)
}
pub(super) fn after_key(&self, v: &Value, col: usize) -> Result<String, EditError> {
if self.flat(v) {
return Ok(format!(" {}", self.flow(v)?));
}
let mut m = Mapping::new();
m.insert(Value::from("x"), v.clone());
let text = self.emit_root(&Value::Mapping(m))?;
let rest = text.strip_prefix("x:").ok_or(EditError::Internal)?;
Ok(shift(rest, col))
}
pub(super) fn after_dash(&self, v: &Value, col: usize) -> Result<String, EditError> {
if self.flat(v) {
return Ok(format!(" {}", self.flow(v)?));
}
let text = self.emit_root(&Value::Sequence(vec![v.clone()].into()))?;
let rest = text.strip_prefix('-').ok_or(EditError::Internal)?;
Ok(shift(rest, col))
}
pub(super) fn entry(&self, key: &Value, v: &Value, col: usize) -> Result<String, EditError> {
if self.flat(key) || self.flat(v) {
return self.flow_entry(key, v);
}
let mut m = Mapping::new();
m.insert(key.clone(), v.clone());
Ok(shift(&self.emit_root(&Value::Mapping(m))?, col))
}
pub(super) fn item(&self, v: &Value, col: usize) -> Result<String, EditError> {
if self.flat(v) {
return Ok(format!("- {}", self.flow(v)?));
}
let text = self.emit_root(&Value::Sequence(vec![v.clone()].into()))?;
Ok(shift(&text, col))
}
pub(super) fn own(&self, v: &Value, col: usize) -> Result<String, EditError> {
Ok(shift(&self.root(v)?, col))
}
pub(super) fn flow_entry(&self, key: &Value, v: &Value) -> Result<String, EditError> {
let mut m = Mapping::new();
m.insert(key.clone(), v.clone());
let text = self.flow(&Value::Mapping(m))?;
let inner = text
.strip_prefix('{')
.and_then(|t| t.strip_suffix('}'))
.ok_or(EditError::Internal)?;
Ok(inner.to_owned())
}
pub(super) fn key(&self, key: &str, flow: bool) -> Result<String, EditError> {
let mut out = String::new();
let ok = if flow {
self.flow_string(&mut out, key);
out.len() <= MAX_IMPLICIT_KEY && !out.contains('\n')
} else {
self.rules.write_implicit_key(&mut out, key, None)
};
if ok {
Ok(out)
} else {
Err(EditError::Unsupported {
at: oneyaml_core::Path::new(),
reason: "a key too long or multi-line for an implicit key",
})
}
}
pub(super) fn keep_style(&self, text: &str, style: ScalarStyle) -> Option<String> {
let block = BlockIndent {
content: 0,
indicator: None,
};
let chosen = self.rules.string_style(text, block);
let st = match (style, chosen) {
(ScalarStyle::DoubleQuoted, Style::Plain | Style::Single | Style::Double) => {
Style::Double
}
(ScalarStyle::SingleQuoted, Style::Plain | Style::Single) => Style::Single,
(ScalarStyle::Plain, Style::Plain) if !text.contains([',', '[', ']', '{', '}']) => {
Style::Plain
}
_ => return None,
};
let mut out = String::new();
self.rules.write_string(&mut out, text, st, block);
Some(out)
}
pub(super) fn flow(&self, v: &Value) -> Result<String, EditError> {
v.check_depth(&self.limits)
.map_err(|e| EditError::rejected(&e.into_error()))?;
let mut out = String::new();
let mut stack = vec![Task::Node(v)];
while let Some(t) = stack.pop() {
match t {
Task::Text(s) => out.push_str(s),
Task::Node(v) => self.flow_node(&mut out, v, &mut stack)?,
Task::Key(k) => {
let scalar = !matches!(k.untag_ref(), Value::Sequence(_) | Value::Mapping(_));
if scalar {
let start = out.len();
self.flow_node(&mut out, k, &mut stack)?;
if out.len().saturating_sub(start) > MAX_IMPLICIT_KEY {
out.insert_str(start, "? ");
}
} else {
out.push_str("? ");
self.flow_node(&mut out, k, &mut stack)?;
}
}
Task::Seq(mut it, first) => match it.next() {
Some(x) => {
if !first {
out.push_str(", ");
}
stack.push(Task::Seq(it, false));
stack.push(Task::Node(x));
}
None => out.push(']'),
},
Task::Map(mut it, first) => match it.next() {
Some((k, x)) => {
if !first {
out.push_str(", ");
}
stack.push(Task::Map(it, false));
stack.push(Task::Node(x));
stack.push(Task::Text(": "));
stack.push(Task::Key(k));
}
None => out.push('}'),
},
}
}
Ok(out)
}
fn flow_node<'a>(
&self,
out: &mut String,
v: &'a Value,
stack: &mut Vec<Task<'a>>,
) -> Result<(), EditError> {
let (tag, content) = match v {
Value::Tagged(t) => (Some(t.tag.parts()), &t.value),
other => (None, other),
};
if let Some(tag) = tag {
if scalar::write_tag(out, tag).is_err() {
return Err(unrepresentable(
"a tag with no spec-valid form that reads back as the same tag",
));
}
out.push(' ');
match content {
Value::String(_) | Value::Sequence(_) | Value::Mapping(_) => {}
Value::Tagged(_) => {
return Err(unrepresentable(
"a tagged value inside a tagged value: a YAML node has one tag",
));
}
_ => {
return Err(unrepresentable(
"a custom-tagged non-string scalar: it always reads back as a string",
));
}
}
}
match content {
Value::Null => scalar::write_null(out),
Value::Bool(b) => scalar::write_bool(out, *b),
Value::Number(n) => self
.rules
.write_number(out, n)
.map_err(|e| EditError::rejected(&e))?,
Value::String(s) => self.flow_string(out, s),
Value::Sequence(s) => {
if s.is_empty() {
out.push_str("[]");
} else {
out.push('[');
stack.push(Task::Seq(s.iter(), true));
}
}
Value::Mapping(m) => {
if m.is_empty() {
out.push_str("{}");
} else {
out.push('{');
stack.push(Task::Map(m.iter(), true));
}
}
Value::Tagged(_) => return Err(EditError::Internal),
}
Ok(())
}
fn flow_string(&self, out: &mut String, s: &str) {
let block = BlockIndent {
content: 0,
indicator: None,
};
let style = match self.rules.string_style(s, block) {
Style::Literal => Style::Double,
Style::Plain if s.contains([',', '[', ']', '{', '}']) => {
if self.double {
Style::Double
} else {
Style::Single
}
}
other => other,
};
self.rules.write_string(out, s, style, block);
}
}
pub(super) fn multiline(v: &Value) -> bool {
let mut stack = vec![v];
while let Some(v) = stack.pop() {
match v {
Value::String(s) => {
if s.contains('\n') {
return true;
}
}
Value::Sequence(s) => stack.extend(s.iter()),
Value::Mapping(m) => {
for (k, x) in m.iter() {
stack.push(k);
stack.push(x);
}
}
Value::Tagged(t) => stack.push(&t.value),
_ => {}
}
}
false
}
fn lines(text: &str) -> Vec<(&str, &str)> {
let mut out = Vec::new();
let mut rest = text;
loop {
match rest.find(['\n', '\r']) {
None => {
out.push((rest, ""));
return out;
}
Some(i) => {
let len = if rest.get(i..).is_some_and(|t| t.starts_with("\r\n")) {
2
} else {
1
};
let end = i.saturating_add(len);
out.push((rest.get(..i).unwrap_or(""), rest.get(i..end).unwrap_or("")));
rest = rest.get(end..).unwrap_or("");
}
}
}
}
pub(super) fn shift(text: &str, by: usize) -> String {
if by == 0 || !text.contains(['\n', '\r']) {
return text.to_owned();
}
let mut out = String::with_capacity(text.len());
for (i, (line, brk)) in lines(text).into_iter().enumerate() {
if i > 0 && !line.is_empty() {
out.extend(std::iter::repeat_n(' ', by));
}
out.push_str(line);
out.push_str(brk);
}
out
}
pub(super) fn reindent(text: &str, from: usize, to: usize) -> Option<String> {
if from <= to {
return Some(shift(text, to.saturating_sub(from)));
}
let by = from.saturating_sub(to);
let mut out = String::with_capacity(text.len());
for (i, (line, brk)) in lines(text).into_iter().enumerate() {
if i == 0 {
out.push_str(line);
out.push_str(brk);
continue;
}
let t = line.trim_start_matches(' ');
let lead = line.len().saturating_sub(t.len());
if lead >= by {
out.push_str(line.get(by..)?);
} else {
if !(t.is_empty() || t.starts_with('#')) {
return None;
}
if !t.is_empty() {
out.extend(std::iter::repeat_n(' ', to));
out.push_str(t);
}
}
out.push_str(brk);
}
Some(out)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use super::*;
#[test]
fn shifting_and_reindenting() {
assert_eq!(shift("a:\n b: 1\n\n c", 2), "a:\n b: 1\n\n c");
assert_eq!(shift("a", 4), "a");
assert_eq!(reindent("a:\n b", 2, 0).unwrap(), "a:\n b");
assert_eq!(reindent("a:\n# c\n b", 2, 0).unwrap(), "a:\n# c\n b");
assert_eq!(reindent("a:\n b", 4, 0), None);
assert_eq!(reindent("a:\n b", 0, 2).unwrap(), "a:\n b");
assert_eq!(shift("a:\r b\r\n c", 2), "a:\r b\r\n c");
assert_eq!(reindent("b:\r c: 1", 2, 0).unwrap(), "b:\rc: 1");
assert_eq!(reindent("b:\r c: 1", 4, 0), None);
}
#[test]
fn flow_text() {
let r = Render::new(&Settings::new(), &Emit::new(), "\n").unwrap();
let v: Value =
crate::from_str("{a: [1, 'x,y', null], b: {}, 'c d': [], e: \"l1\\nl2\"}").unwrap();
assert_eq!(
r.flow(&v).unwrap(),
"{a: [1, 'x,y', null], b: {}, c d: [], e: \"l1\\nl2\"}"
);
let k: Value = crate::from_str("? [1]\n: 2").unwrap();
assert_eq!(r.flow(&k).unwrap(), "{? [1]: 2}");
let t: Value = crate::from_str("!t {a: !u x}").unwrap();
assert_eq!(r.flow(&t).unwrap(), "!t {a: !u x}");
let bad = Value::Tagged(Box::new(crate::TaggedValue {
tag: crate::Tag::new("!t"),
value: Value::from(1),
}));
assert!(matches!(r.flow(&bad), Err(EditError::Rejected { .. })));
}
}