use crate::ast::Value;
use crate::error::Result;
use crate::plugin::ParserPlugin;
use rustc_hash::FxHashMap;
use std::any::Any;
#[derive(Debug, Clone)]
pub struct Comment {
pub text: String,
pub line: usize,
pub column: usize,
pub is_multiline: bool,
}
pub struct CommentPreservationPlugin {
comments: FxHashMap<String, Vec<Comment>>,
all_comments: Vec<Comment>,
current_line: usize,
current_column: usize,
}
impl CommentPreservationPlugin {
pub fn new() -> Self {
CommentPreservationPlugin {
comments: FxHashMap::default(),
all_comments: Vec::new(),
current_line: 1,
current_column: 1,
}
}
pub fn add_comment(&mut self, text: String, path: &str, is_multiline: bool) {
let comment = Comment {
text,
line: self.current_line,
column: self.current_column,
is_multiline,
};
self.all_comments.push(comment.clone());
self.comments
.entry(path.to_string())
.or_default()
.push(comment);
}
pub fn all_comments(&self) -> &[Comment] {
&self.all_comments
}
pub fn comments_at(&self, path: &str) -> Option<&[Comment]> {
self.comments.get(path).map(|v| v.as_slice())
}
pub fn update_position(&mut self, text: &str) {
for ch in text.chars() {
if ch == '\n' {
self.current_line += 1;
self.current_column = 1;
} else {
self.current_column += 1;
}
}
}
pub fn comments_to_value(&self) -> Value {
let mut result = Vec::new();
for comment in &self.all_comments {
let mut obj = FxHashMap::default();
obj.insert("text".to_string(), Value::String(comment.text.clone()));
obj.insert(
"line".to_string(),
Value::Number(crate::ast::Number::Integer(comment.line as i64)),
);
obj.insert(
"column".to_string(),
Value::Number(crate::ast::Number::Integer(comment.column as i64)),
);
obj.insert("multiline".to_string(), Value::Bool(comment.is_multiline));
result.push(Value::Object(obj));
}
Value::Array(result)
}
}
impl Default for CommentPreservationPlugin {
fn default() -> Self {
Self::new()
}
}
impl ParserPlugin for CommentPreservationPlugin {
fn name(&self) -> &str {
"comment_preservation"
}
fn on_parse_end(&mut self, _value: &Value) -> Result<()> {
Ok(())
}
fn transform_value(&mut self, value: &mut Value, path: &str) -> Result<()> {
match value {
Value::Object(obj) => {
if let Some(comments) = self.comments_at(path) {
if !comments.is_empty() && !obj.contains_key("_comments") {
let comment_values: Vec<Value> = comments
.iter()
.map(|c| Value::String(c.text.clone()))
.collect();
obj.insert("_comments".to_string(), Value::Array(comment_values));
}
}
for (key, val) in obj.iter_mut() {
let child_path = format!("{path}.{key}");
self.transform_value(val, &child_path)?;
}
}
Value::Array(arr) => {
for (i, val) in arr.iter_mut().enumerate() {
let child_path = format!("{path}[{i}]");
self.transform_value(val, &child_path)?;
}
}
_ => {}
}
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_comment_storage() {
let mut plugin = CommentPreservationPlugin::new();
plugin.add_comment("This is a test comment".to_string(), "$.foo", false);
plugin.add_comment("Another comment".to_string(), "$.bar", true);
assert_eq!(plugin.all_comments().len(), 2);
assert_eq!(plugin.comments_at("$.foo").unwrap().len(), 1);
assert_eq!(
plugin.comments_at("$.foo").unwrap()[0].text,
"This is a test comment"
);
}
#[test]
fn test_position_tracking() {
let mut plugin = CommentPreservationPlugin::new();
plugin.update_position("hello\nworld");
assert_eq!(plugin.current_line, 2);
assert_eq!(plugin.current_column, 6);
plugin.add_comment("test".to_string(), "$", false);
let comment = &plugin.all_comments()[0];
assert_eq!(comment.line, 2);
assert_eq!(comment.column, 6);
}
#[test]
fn test_comments_to_value() {
let mut plugin = CommentPreservationPlugin::new();
plugin.add_comment("Comment 1".to_string(), "$", false);
plugin.update_position("\n");
plugin.add_comment("Comment 2".to_string(), "$", true);
let value = plugin.comments_to_value();
if let Value::Array(arr) = value {
assert_eq!(arr.len(), 2);
if let Value::Object(obj) = &arr[0] {
assert_eq!(
obj.get("text"),
Some(&Value::String("Comment 1".to_string()))
);
assert_eq!(obj.get("multiline"), Some(&Value::Bool(false)));
}
} else {
panic!("Expected array");
}
}
}