use crate::config::FormatConfig;
use crate::doc::Doc;
use crate::printer::Printer;
use eure_document::document::node::{NodeArray, NodeMap, NodeTuple, NodeValue};
use eure_document::document::{EureDocument, NodeId};
use eure_document::identifier::Identifier;
use eure_document::path::ArrayIndexKind;
use eure_document::source::{
ArrayElementSource, BindSource, BindingSource, Comment, EureSource, SectionBody, SectionSource,
SourceDocument, SourceId, SourceKey, SourcePathSegment, StringStyle, Trivia,
};
use eure_document::text::{Language, SyntaxHint};
use eure_document::value::{ObjectKey, PartialObjectKey, PrimitiveValue};
pub fn build_source_doc(source: &SourceDocument) -> Doc {
SourceDocBuilder::new(source).build()
}
pub fn format_source_document(source: &SourceDocument) -> String {
let doc = build_source_doc(source);
Printer::new(FormatConfig::default()).print(&doc)
}
struct SourceDocBuilder<'a> {
source: &'a SourceDocument,
}
impl<'a> SourceDocBuilder<'a> {
fn new(source: &'a SourceDocument) -> Self {
Self { source }
}
fn doc(&self) -> &EureDocument {
&self.source.document
}
fn get_source(&self, id: SourceId) -> &EureSource {
self.source.source(id)
}
fn build(&self) -> Doc {
self.build_eure_source(self.source.root_source())
}
fn build_eure_source(&self, eure: &EureSource) -> Doc {
let mut parts = Vec::new();
if !eure.leading_trivia.is_empty() {
parts.push(self.build_trivia(&eure.leading_trivia));
}
if let Some(node_id) = eure.value {
parts.push(
Doc::text("= ")
.concat(self.build_value(node_id))
.concat(Doc::hardline()),
);
}
for binding in &eure.bindings {
parts.push(self.build_binding(binding));
}
for section in &eure.sections {
parts.push(self.build_section(section));
}
if !eure.trailing_trivia.is_empty() {
parts.push(self.build_trivia(&eure.trailing_trivia));
}
Doc::concat_all(parts)
}
fn build_trivia(&self, trivia: &[Trivia]) -> Doc {
let mut parts = Vec::new();
for item in trivia {
match item {
Trivia::Comment(comment) => {
parts.push(self.build_comment(comment));
}
Trivia::BlankLine => {
parts.push(Doc::hardline());
}
}
}
Doc::concat_all(parts)
}
fn build_comment(&self, comment: &Comment) -> Doc {
match comment {
Comment::Line(s) => {
if s.is_empty() {
Doc::text("//").concat(Doc::hardline())
} else {
Doc::text("// ")
.concat(Doc::text(s.clone()))
.concat(Doc::hardline())
}
}
Comment::Block(s) => {
if s.is_empty() {
Doc::text("/**/").concat(Doc::hardline())
} else {
Doc::text("/* ")
.concat(Doc::text(s.clone()))
.concat(Doc::text(" */"))
.concat(Doc::hardline())
}
}
}
}
fn build_array_comment(&self, comment: &Comment) -> Doc {
match comment {
Comment::Line(s) => {
if s.is_empty() {
Doc::text("//")
} else {
Doc::text("// ").concat(Doc::text(s.clone()))
}
}
Comment::Block(s) => {
if s.is_empty() {
Doc::text("/**/")
} else {
Doc::text("/* ")
.concat(Doc::text(s.clone()))
.concat(Doc::text(" */"))
}
}
}
}
fn build_trailing_comment(&self, comment: &Comment) -> Doc {
match comment {
Comment::Line(s) => {
if s.is_empty() {
Doc::text(" //")
} else {
Doc::text(" // ").concat(Doc::text(s.clone()))
}
}
Comment::Block(s) => {
if s.is_empty() {
Doc::text(" /**/")
} else {
Doc::text(" /* ")
.concat(Doc::text(s.clone()))
.concat(Doc::text(" */"))
}
}
}
}
fn build_binding(&self, binding: &BindingSource) -> Doc {
let mut parts = Vec::new();
if !binding.trivia_before.is_empty() {
parts.push(self.build_trivia(&binding.trivia_before));
}
let path_doc = self.build_path(&binding.path);
let body_doc = match &binding.bind {
BindSource::Value(node_id) => Doc::text(" = ").concat(self.build_value(*node_id)),
BindSource::Array { node, elements } => {
Doc::text(" = ").concat(self.build_array_with_trivia(*node, elements))
}
BindSource::Block(source_id) => {
let inner = self.build_eure_source(self.get_source(*source_id));
self.build_braced_block(Doc::text(""), inner)
}
};
let mut doc = path_doc.concat(body_doc);
if let Some(comment) = &binding.trailing_comment {
doc = doc.concat(self.build_trailing_comment(comment));
}
parts.push(doc.concat(Doc::hardline()));
Doc::concat_all(parts)
}
fn build_section(&self, section: &SectionSource) -> Doc {
let mut parts = Vec::new();
if !section.trivia_before.is_empty() {
parts.push(self.build_trivia(§ion.trivia_before));
}
let mut header = Doc::text("@ ").concat(self.build_path(§ion.path));
if let Some(comment) = §ion.trailing_comment {
header = header.concat(self.build_trailing_comment(comment));
}
let section_doc = match §ion.body {
SectionBody::Items { value, bindings } => {
if let Some(node_id) = value
&& bindings.is_empty()
{
parts.push(
header
.concat(Doc::text(" = "))
.concat(self.build_value(*node_id))
.concat(Doc::hardline()),
);
return Doc::concat_all(parts);
}
let mut body_parts = Vec::new();
if let Some(node_id) = value {
body_parts.push(
Doc::text("= ")
.concat(self.build_value(*node_id))
.concat(Doc::hardline()),
);
}
for binding in bindings {
body_parts.push(self.build_binding(binding));
}
header
.concat(Doc::hardline())
.concat(Doc::concat_all(body_parts))
}
SectionBody::Block(source_id) => {
let inner = self.build_eure_source(self.get_source(*source_id));
self.build_braced_block(header, inner)
.concat(Doc::hardline())
}
};
parts.push(section_doc);
Doc::concat_all(parts)
}
fn build_braced_block(&self, header: Doc, inner: Doc) -> Doc {
let (inner, removed) = strip_one_trailing_hardline(inner);
if !removed || matches!(inner, Doc::Nil) {
return header.concat(Doc::text(" {}"));
}
header
.concat(Doc::text(" {"))
.concat(Doc::indent(Doc::hardline().concat(inner)))
.concat(Doc::hardline())
.concat(Doc::text("}"))
}
fn build_path(&self, path: &[SourcePathSegment]) -> Doc {
let mut result = Doc::Nil;
for (i, segment) in path.iter().enumerate() {
if i > 0 {
result = result.concat(Doc::text("."));
}
result = result.concat(self.build_key(&segment.key));
if let Some(index) = &segment.array {
result = result.concat(Doc::text("["));
match index {
ArrayIndexKind::Push => {}
ArrayIndexKind::Current => {
result = result.concat(Doc::text("^"));
}
ArrayIndexKind::Specific(n) => {
result = result.concat(Doc::text(n.to_string()));
}
}
result = result.concat(Doc::text("]"));
}
}
result
}
fn build_key(&self, key: &SourceKey) -> Doc {
match key {
SourceKey::Ident(s) => Doc::text(s.as_ref()),
SourceKey::Extension(s) => Doc::text("$").concat(Doc::text(s.as_ref())),
SourceKey::Hole(None) => Doc::text("!"),
SourceKey::Hole(Some(label)) => Doc::text("!").concat(Doc::text(label.as_ref())),
SourceKey::String(s, style) => match style {
StringStyle::Quoted => Doc::text("\"")
.concat(Doc::text(escape_string(s)))
.concat(Doc::text("\"")),
StringStyle::Literal => Doc::text("'")
.concat(Doc::text(s.clone()))
.concat(Doc::text("'")),
StringStyle::DelimitedLitStr(level) => {
let delim_open: String = "<".repeat(*level as usize);
let delim_close: String = ">".repeat(*level as usize);
Doc::text(format!("{delim_open}'{s}'{delim_close}"))
}
StringStyle::DelimitedCode(level) => {
let delim_open: String = "<".repeat(*level as usize);
let delim_close: String = ">".repeat(*level as usize);
Doc::text(format!("{delim_open}`{s}`{delim_close}"))
}
},
SourceKey::Integer(n) => Doc::text(n.to_string()),
SourceKey::Tuple(keys) => {
let inner = Doc::join(keys.iter().map(|k| self.build_key(k)), Doc::text(", "));
Doc::text("(").concat(inner).concat(Doc::text(")"))
}
SourceKey::TupleIndex(n) => Doc::text("#").concat(Doc::text(n.to_string())),
}
}
fn build_value(&self, node_id: NodeId) -> Doc {
let node = self.doc().node(node_id);
match &node.content {
NodeValue::Hole(_) => Doc::text("null"),
NodeValue::Primitive(prim) => self.build_primitive(prim),
NodeValue::Array(arr) => self.build_array(node_id, arr),
NodeValue::Tuple(tuple) => self.build_tuple(tuple),
NodeValue::Map(map) => self.build_map(map),
NodeValue::PartialMap(map) => self.build_partial_map(map),
}
}
fn build_primitive(&self, prim: &PrimitiveValue) -> Doc {
match prim {
PrimitiveValue::Null => Doc::text("null"),
PrimitiveValue::Bool(b) => Doc::text(if *b { "true" } else { "false" }),
PrimitiveValue::Integer(n) => Doc::text(n.to_string()),
PrimitiveValue::F64(f) => self.build_f64(*f),
PrimitiveValue::F32(f) => self.build_f32(*f),
PrimitiveValue::Text(text) => self.build_text(text),
}
}
fn build_f64(&self, f: f64) -> Doc {
if f.is_nan() {
Doc::text("nan")
} else if f.is_infinite() {
if f.is_sign_positive() {
Doc::text("inf")
} else {
Doc::text("-inf")
}
} else {
let s = f.to_string();
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
Doc::text(s).concat(Doc::text(".0"))
} else {
Doc::text(s)
}
}
}
fn build_f32(&self, f: f32) -> Doc {
if f.is_nan() {
Doc::text("nan")
} else if f.is_infinite() {
if f.is_sign_positive() {
Doc::text("inf")
} else {
Doc::text("-inf")
}
} else {
Doc::text(f.to_string())
}
}
fn build_text(&self, text: &eure_document::text::Text) -> Doc {
let is_block = matches!(
text.syntax_hint,
Some(SyntaxHint::Block)
| Some(SyntaxHint::Block3)
| Some(SyntaxHint::Block4)
| Some(SyntaxHint::Block5)
| Some(SyntaxHint::Block6)
);
if is_block {
self.build_block_text(text)
} else {
self.build_inline_text(text)
}
}
fn build_block_text(&self, text: &eure_document::text::Text) -> Doc {
let backticks = match text.syntax_hint {
Some(SyntaxHint::Block6) => "``````",
Some(SyntaxHint::Block5) => "`````",
Some(SyntaxHint::Block4) => "````",
_ => "```",
};
let mut doc = Doc::text(backticks);
if let Language::Other(lang) = &text.language {
doc = doc.concat(Doc::text(lang.clone()));
}
doc = doc.concat(Doc::text("\n"));
doc = doc.concat(Doc::text(text.content.clone()));
if !text.content.ends_with('\n') {
doc = doc.concat(Doc::text("\n"));
}
doc.concat(Doc::text(backticks))
}
fn build_inline_text(&self, text: &eure_document::text::Text) -> Doc {
match &text.language {
Language::Plaintext => Doc::text("\"")
.concat(Doc::text(escape_string(&text.content)))
.concat(Doc::text("\"")),
Language::Implicit => Doc::text("`")
.concat(Doc::text(text.content.clone()))
.concat(Doc::text("`")),
Language::Other(lang) => Doc::text(lang.clone())
.concat(Doc::text("`"))
.concat(Doc::text(text.content.clone()))
.concat(Doc::text("`")),
}
}
fn build_array(&self, node_id: NodeId, arr: &NodeArray) -> Doc {
if arr.is_empty() {
return Doc::text("[]");
}
let force_multiline = self.source.is_multiline_array(node_id);
if force_multiline {
let elements = Doc::join(
arr.iter()
.map(|&id| self.build_value(id).concat(Doc::text(","))),
Doc::hardline(),
);
Doc::text("[")
.concat(Doc::indent(Doc::hardline().concat(elements)))
.concat(Doc::hardline())
.concat(Doc::text("]"))
} else {
let elements = Doc::join(
arr.iter().map(|&id| self.build_value(id)),
Doc::text(",").concat(Doc::line()),
);
Doc::group(
Doc::text("[")
.concat(Doc::softline())
.concat(Doc::indent(elements))
.concat(Doc::softline())
.concat(Doc::text("]")),
)
}
}
fn build_array_with_trivia(&self, node_id: NodeId, elements: &[ArrayElementSource]) -> Doc {
let arr = match &self.doc().node(node_id).content {
NodeValue::Array(arr) => arr,
_ => return self.build_value(node_id), };
if elements.is_empty() {
return self.build_array(node_id, arr);
}
let mut inner_parts = Vec::new();
for (i, elem_source) in elements.iter().enumerate() {
for trivia in &elem_source.trivia_before {
match trivia {
Trivia::Comment(comment) => {
inner_parts.push(self.build_array_comment(comment));
inner_parts.push(Doc::hardline());
}
Trivia::BlankLine => {
inner_parts.push(Doc::hardline());
}
}
}
let value_id = arr.get(elem_source.index).unwrap();
let mut elem_doc = self.build_value(value_id);
elem_doc = elem_doc.concat(Doc::text(","));
if let Some(comment) = &elem_source.trailing_comment {
elem_doc = elem_doc.concat(self.build_trailing_comment(comment));
}
inner_parts.push(elem_doc);
if i < elements.len() - 1 {
inner_parts.push(Doc::hardline());
}
}
Doc::text("[")
.concat(Doc::indent(
Doc::hardline().concat(Doc::concat_all(inner_parts)),
))
.concat(Doc::hardline())
.concat(Doc::text("]"))
}
fn build_tuple(&self, tuple: &NodeTuple) -> Doc {
if tuple.is_empty() {
return Doc::text("()");
}
let elements = Doc::join(
tuple.iter().map(|&id| self.build_value(id)),
Doc::text(", "),
);
Doc::text("(").concat(elements).concat(Doc::text(")"))
}
fn build_map(&self, map: &NodeMap) -> Doc {
if map.is_empty() {
return Doc::text("{}");
}
let entries = Doc::join(
map.iter().map(|(key, &child_id)| {
self.build_object_key(key)
.concat(Doc::text(" => "))
.concat(self.build_value(child_id))
}),
Doc::text(", "),
);
Doc::text("{ ").concat(entries).concat(Doc::text(" }"))
}
fn build_partial_map(&self, map: &eure_document::map::PartialNodeMap) -> Doc {
if map.is_empty() {
return Doc::text("{}");
}
let entries = Doc::join(
map.iter().map(|(key, &child_id)| {
self.build_partial_object_key(key)
.concat(Doc::text(" => "))
.concat(self.build_value(child_id))
}),
Doc::text(", "),
);
Doc::text("{ ").concat(entries).concat(Doc::text(" }"))
}
fn build_object_key(&self, key: &ObjectKey) -> Doc {
match key {
ObjectKey::String(s) => {
if s.parse::<Identifier>().is_ok() {
Doc::text(s.clone())
} else {
Doc::text("\"")
.concat(Doc::text(escape_string(s)))
.concat(Doc::text("\""))
}
}
ObjectKey::Number(n) => Doc::text(n.to_string()),
ObjectKey::Tuple(keys) => {
let inner = Doc::join(
keys.iter().map(|k| self.build_object_key(k)),
Doc::text(", "),
);
Doc::text("(").concat(inner).concat(Doc::text(")"))
}
}
}
fn build_partial_object_key(&self, key: &PartialObjectKey) -> Doc {
match key {
PartialObjectKey::String(s) => {
if s.parse::<Identifier>().is_ok() {
Doc::text(s.clone())
} else {
Doc::text("\"")
.concat(Doc::text(escape_string(s)))
.concat(Doc::text("\""))
}
}
PartialObjectKey::Number(n) => Doc::text(n.to_string()),
PartialObjectKey::Hole(None) => Doc::text("!"),
PartialObjectKey::Hole(Some(label)) => Doc::text("!").concat(Doc::text(label.as_ref())),
PartialObjectKey::Tuple(keys) => {
let inner = Doc::join(
keys.iter().map(|k| self.build_partial_object_key(k)),
Doc::text(", "),
);
Doc::text("(").concat(inner).concat(Doc::text(")"))
}
}
}
}
fn strip_one_trailing_hardline(doc: Doc) -> (Doc, bool) {
match doc {
Doc::HardLine => (Doc::Nil, true),
Doc::Concat(left, right) => {
let left = *left;
let right = *right;
let (new_right, removed) = strip_one_trailing_hardline(right.clone());
if removed {
return (left.concat(new_right), true);
}
let (new_left, removed) = strip_one_trailing_hardline(left);
(new_left.concat(right), removed)
}
other => (other, false),
}
}
fn escape_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result
}