mod config;
mod json;
mod number;
mod text;
pub use text::key_needs_quoting;
use crate::parse::facts::{self, NodeId};
use crate::parse::tree::Children;
use crate::parse::{
AttachedComments, Comment, CommentPlacement, OutputFacts, PathSegment, ValueFacts,
};
use crate::value::UclValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Format {
Json,
JsonCompact,
Config,
Yaml,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
Libucl,
RoundTrip,
}
#[derive(Debug, Clone, Copy)]
pub struct Emitter<'a> {
format: Format,
facts: Option<&'a OutputFacts>,
comments: Option<(&'a [Comment], &'a [AttachedComments])>,
mode: Mode,
}
impl<'a> Emitter<'a> {
pub fn new(format: Format) -> Self {
Self {
format,
facts: None,
comments: None,
mode: Mode::Libucl,
}
}
pub(crate) fn round_trip(mut self) -> Self {
self.mode = Mode::RoundTrip;
self
}
pub fn format(&self) -> Format {
self.format
}
pub fn with_facts(mut self, facts: &'a OutputFacts) -> Self {
self.facts = Some(facts);
self
}
pub fn with_comments(
mut self,
comments: &'a [Comment],
attached: &'a [AttachedComments],
) -> Self {
self.comments = Some((comments, attached));
self
}
pub fn emit(&self, value: &UclValue) -> String {
self.run(value).out
}
pub(crate) fn try_emit(&self, value: &UclValue) -> Result<String, String> {
let writer = self.run(value);
match writer.error {
Some(error) => Err(error),
None => Ok(writer.out),
}
}
fn run(&self, value: &UclValue) -> Writer<'a> {
let exact = self.mode == Mode::RoundTrip;
let comments = match (self.format, self.comments) {
(Format::Config, Some((comments, attached))) if !exact => {
CommentTree::new(comments, attached)
}
_ => CommentTree::default(),
};
let facts = self.facts.filter(|f| !f.is_empty() && !exact);
let track = facts.is_some() || !comments.is_empty();
let mut writer = Writer {
out: String::new(),
track,
cursor: vec![(
facts.map(|_| facts::ROOT),
(!comments.is_empty()).then_some(0),
)],
facts,
comments,
mode: self.mode,
json: exact && matches!(self.format, Format::Json | Format::JsonCompact),
error: None,
};
if exact && !matches!(value, UclValue::Object(_) | UclValue::Array(_)) {
writer.fail(format!(
"a root {}: a UCL document is an object or an array (spec §1.1)",
value.type_name()
));
}
if exact && crate::value::nesting(value) > crate::parse::MAX_NESTING {
writer.fail(format!(
"a value nested more than {} containers deep, the root included (spec §11.2)",
crate::parse::MAX_NESTING
));
return writer;
}
match self.format {
Format::Json => writer.json_root(value, json::Style::Json),
Format::JsonCompact => writer.json_root(value, json::Style::Compact),
Format::Yaml => writer.json_root(value, json::Style::Yaml),
Format::Config => writer.config_root(value),
}
writer
}
}
pub fn to_json(value: &UclValue) -> String {
Emitter::new(Format::Json).emit(value)
}
pub fn to_json_compact(value: &UclValue) -> String {
Emitter::new(Format::JsonCompact).emit(value)
}
pub fn to_config(value: &UclValue) -> String {
Emitter::new(Format::Config).emit(value)
}
pub fn to_yaml(value: &UclValue) -> String {
Emitter::new(Format::Yaml).emit(value)
}
#[derive(Debug, Default)]
struct CommentTree<'a> {
nodes: Vec<CommentNode<'a>>,
}
#[derive(Debug, Default)]
struct CommentNode<'a> {
comments: Option<(CommentPlacement, Vec<&'a str>)>,
children: Children,
}
impl<'a> CommentTree<'a> {
fn new(comments: &'a [Comment], attached: &'a [AttachedComments]) -> Self {
let mut tree = Self::default();
for group in attached {
let texts = group
.comments
.iter()
.filter_map(|&i| comments.get(i).map(|c| c.text.as_str()))
.collect();
let mut node = tree.root();
for segment in &group.path {
node = tree.child_or_insert(node, segment);
}
tree.nodes[node].comments = Some((group.placement, texts));
}
tree
}
fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
fn root(&mut self) -> usize {
if self.nodes.is_empty() {
self.nodes.push(CommentNode::default());
}
0
}
fn child_or_insert(&mut self, node: usize, segment: &PathSegment) -> usize {
if let Some(child) = self.nodes[node].children.get(segment) {
return child;
}
let child = self.nodes.len();
self.nodes.push(CommentNode::default());
self.nodes[node].children.insert(segment, child);
child
}
}
struct Writer<'a> {
out: String,
facts: Option<&'a OutputFacts>,
comments: CommentTree<'a>,
track: bool,
cursor: Vec<(Option<NodeId>, Option<usize>)>,
mode: Mode,
json: bool,
error: Option<String>,
}
impl<'a> Writer<'a> {
fn fail(&mut self, error: String) {
if self.error.is_none() {
self.error = Some(error);
}
}
fn indent(&mut self, depth: usize) {
for _ in 0..depth {
self.out.push_str(" ");
}
}
fn here(&self) -> (Option<NodeId>, Option<usize>) {
*self.cursor.last().expect("the root is entered")
}
fn enter_key(&mut self, key: &str, index: usize) {
if self.track {
let (fact, comment) = self.here();
let fact = fact.and_then(|n| self.facts?.child_key(n, key, index));
let comment = comment.and_then(|n| self.comments.nodes[n].children.key(key, index));
self.cursor.push((fact, comment));
}
}
fn enter_index(&mut self, index: usize) {
if self.track {
let (fact, comment) = self.here();
let fact = fact.and_then(|n| self.facts?.child_element(n, index));
let comment = comment.and_then(|n| self.comments.nodes[n].children.element(index));
self.cursor.push((fact, comment));
}
}
fn leave(&mut self) {
if self.track {
self.cursor.pop();
}
}
fn facts(&self) -> Option<&ValueFacts> {
let (node, _) = self.here();
self.facts?.facts_of(node?)
}
fn write_key(&mut self, entry_key: &str) {
if self.mode == Mode::RoundTrip {
self.exact_key(entry_key, true);
return;
}
let (spelling, quoted) = match self.facts() {
Some(facts) => {
let spelling = facts.key_spelling.as_deref().unwrap_or(entry_key);
let quoted = facts
.key_quoted
.unwrap_or_else(|| key_needs_quoting(spelling));
(spelling.to_owned(), quoted)
}
None => (entry_key.to_owned(), key_needs_quoting(entry_key)),
};
text::write_key(&mut self.out, &spelling, quoted);
}
fn comments(&self, placement: CommentPlacement) -> Vec<&'a str> {
let (_, node) = self.here();
match node.and_then(|n| self.comments.nodes[n].comments.as_ref()) {
Some((p, texts)) if *p == placement => texts.clone(),
_ => Vec::new(),
}
}
fn exact_key(&mut self, key: &str, bare_allowed: bool) {
if key.is_empty() {
self.fail("the empty key, which parsing rejects (spec §3.2, §10.8)".to_owned());
} else if bare_allowed && text::is_bare_key(key) {
self.out.push_str(key);
} else {
text::write_escaped_string(&mut self.out, key);
}
}
fn exact_scalar(&mut self, value: &UclValue, config: bool) {
let result = match value {
UclValue::Integer(i) => {
use std::fmt::Write;
let _ = write!(self.out, "{i}");
Ok(())
}
UclValue::Float(f) if self.json => {
number::write_json_number(&mut self.out, *f, "float")
}
UclValue::Time(t) if self.json => number::write_json_number(&mut self.out, *t, "time"),
UclValue::Float(f) => number::write_exact_float(&mut self.out, *f),
UclValue::Time(t) => number::write_exact_time(&mut self.out, *t),
UclValue::String(s) if config => text::write_exact_config_string(&mut self.out, s),
UclValue::String(s) => text::write_exact_double_quoted(&mut self.out, s),
UclValue::Boolean(b) => {
self.out.push_str(if *b { "true" } else { "false" });
Ok(())
}
UclValue::Null => {
self.out.push_str("null");
Ok(())
}
UclValue::Object(_) | UclValue::Array(_) => {
unreachable!("containers are written by the format")
}
};
if let Err(error) = result {
self.fail(error);
}
}
fn scalar(&mut self, value: &UclValue) {
if self.mode == Mode::RoundTrip {
self.exact_scalar(value, false);
return;
}
match value {
UclValue::Integer(i) => {
use std::fmt::Write;
let _ = write!(self.out, "{i}");
}
UclValue::Float(f) | UclValue::Time(f) => number::write_float(&mut self.out, *f),
UclValue::String(s) => text::write_json_string(&mut self.out, s),
UclValue::Boolean(b) => self.out.push_str(if *b { "true" } else { "false" }),
UclValue::Null => self.out.push_str("null"),
UclValue::Object(_) | UclValue::Array(_) => {
unreachable!("containers are written by the format")
}
}
}
}
#[cfg(test)]
mod tests;