diff --git a/Cargo.lock b/Cargo.lock index 26ff19c..9318a68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,6 +1029,7 @@ dependencies = [ "ryu", "serde", "serde_core", + "serde_json", "unicode-general-category", "yoke", ] @@ -1115,7 +1116,7 @@ dependencies = [ [[package]] name = "duper_zed" -version = "0.0.0" +version = "0.0.1" dependencies = [ "serde", "serde_json", diff --git a/duper/CHANGELOG.md b/duper/CHANGELOG.md index 32ad6c1..37f07b8 100644 --- a/duper/CHANGELOG.md +++ b/duper/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Add `yoke::Yokeable` derive to `DuperValue` (gated behind the `yoke` feature). +- Add `ToJson` visitor (gated behind the `json-compat` feature). ## 0.6.0 (2025-12-23) diff --git a/duper/Cargo.toml b/duper/Cargo.toml index 2ab6cb5..6a0d419 100644 --- a/duper/Cargo.toml +++ b/duper/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["duper", "parser", "serialization"] [features] default = [] ansi = ["dep:owo-colors"] +json-compat = ["dep:serde_json"] serde = ["dep:serde_core"] yoke = ["dep:yoke"] @@ -28,6 +29,7 @@ indexmap = "2" owo-colors = { version = "4", optional = true } ryu = "1" serde_core = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true, features = ["preserve_order"] } unicode-general-category = "1" yoke = { workspace = true, optional = true } diff --git a/duper/src/lib.rs b/duper/src/lib.rs index a1f71cb..06bb1f2 100644 --- a/duper/src/lib.rs +++ b/duper/src/lib.rs @@ -57,4 +57,6 @@ pub use ast::{ pub use parser::DuperParser; #[cfg(feature = "ansi")] pub use visitor::ansi::Ansi; +#[cfg(feature = "json-compat")] +pub use visitor::to_json::ToJson; pub use visitor::{pretty_printer::PrettyPrinter, serializer::Serializer}; diff --git a/duper/src/visitor/mod.rs b/duper/src/visitor/mod.rs index a5a2a66..942f0ab 100644 --- a/duper/src/visitor/mod.rs +++ b/duper/src/visitor/mod.rs @@ -4,6 +4,8 @@ pub mod ansi; pub mod pretty_printer; pub mod serializer; +#[cfg(feature = "json-compat")] +pub mod to_json; use crate::{ DuperIdentifier, DuperTemporal, DuperValue, diff --git a/duper_website/src/visitor/serde_json.rs b/duper/src/visitor/to_json.rs similarity index 78% rename from duper_website/src/visitor/serde_json.rs rename to duper/src/visitor/to_json.rs index 57750c5..4a7a6b9 100644 --- a/duper_website/src/visitor/serde_json.rs +++ b/duper/src/visitor/to_json.rs @@ -1,12 +1,25 @@ -use base64::{Engine, prelude::BASE64_STANDARD}; -use duper::{ +use crate::{ DuperFloat, DuperIdentifier, DuperObject, DuperTemporal, DuperValue, visitor::DuperVisitor, }; +use base64::{Engine, prelude::BASE64_STANDARD}; -// A visitor that simplifies Duper values for Serde serializers. -pub(crate) struct SerdeJsonVisitor; +/// A visitor that converts Duper values into JSON values for compatibility. +/// +/// Duper is a superset of JSON, so conversion *from* JSON is trivial. +/// However, convertion *to* JSON requires converting unsupported types +/// (eg. tuples and Temporal values) into JSON-compatible ones. This visitor +/// returns a [`serde_json::Value`] following these rules: +/// +/// - Identifiers, trailing commas, and comments are stripped. +/// - Keys, raw strings, etc. are quoted following JSON rules. +/// - Object ordering is maintained. +/// - Tuples are converted into arrays. +/// - Bytes are converted into Base64 and returned as strings. +/// - Temporal values are converted into strings. +/// - All other values are returned as-is. +pub struct ToJson; -impl DuperVisitor for SerdeJsonVisitor { +impl DuperVisitor for ToJson { type Value = serde_json::Value; fn visit_object<'a>( diff --git a/duper_website/Cargo.toml b/duper_website/Cargo.toml index 4d092e4..a89be93 100644 --- a/duper_website/Cargo.toml +++ b/duper_website/Cargo.toml @@ -14,7 +14,7 @@ crate-type = ["cdylib"] [dependencies] base64 = { workspace = true } -duper = { workspace = true } +duper = { workspace = true, features = ["json-compat"] } js-sys = "0.3" saphyr = "0.0.6" serde_core = { workspace = true } diff --git a/duper_website/docs/components/DuperEditor.vue b/duper_website/docs/components/DuperEditor.vue index 08b5260..d5ce5d5 100644 --- a/duper_website/docs/components/DuperEditor.vue +++ b/duper_website/docs/components/DuperEditor.vue @@ -1,31 +1,40 @@ diff --git a/duper_website/src/lib.rs b/duper_website/src/lib.rs index b58968e..0d8383a 100644 --- a/duper_website/src/lib.rs +++ b/duper_website/src/lib.rs @@ -1,4 +1,4 @@ -use duper::DuperParser; +use duper::{DuperParser, ToJson}; use serde_core::Serialize; use wasm_bindgen::prelude::*; @@ -67,10 +67,8 @@ pub fn convert_duper(value: &str, to: Option) -> Result { - serde_json::to_string_pretty(&duper.accept(&mut visitor::SerdeJsonVisitor {})) - .map_err(|err| JsError::new(&err.to_string())) - } + ConvertTo::Json => serde_json::to_string_pretty(&duper.accept(&mut ToJson {})) + .map_err(|err| JsError::new(&err.to_string())), ConvertTo::Yaml => match duper.accept(&mut visitor::SaphyrVisitor {}) { Ok(yaml) => { let yaml: saphyr::Yaml = (&yaml).into(); diff --git a/duper_website/src/visitor/mod.rs b/duper_website/src/visitor/mod.rs index 3715f15..38861a0 100644 --- a/duper_website/src/visitor/mod.rs +++ b/duper_website/src/visitor/mod.rs @@ -1,7 +1,13 @@ mod saphyr; -mod serde_json; mod toml; pub(crate) use saphyr::SaphyrVisitor; -pub(crate) use serde_json::SerdeJsonVisitor; pub(crate) use toml::TomlVisitor; + +pub(crate) fn clean_temporal(temporal: &str) -> &str { + match temporal.split_once('[') { + Some((start, _)) => start, + None => temporal, + } + .trim() +} diff --git a/duper_website/src/visitor/saphyr.rs b/duper_website/src/visitor/saphyr.rs index 0d4ed53..b160155 100644 --- a/duper_website/src/visitor/saphyr.rs +++ b/duper_website/src/visitor/saphyr.rs @@ -72,7 +72,7 @@ impl DuperVisitor for SaphyrVisitor { fn visit_temporal<'a>(&mut self, temporal: &DuperTemporal<'a>) -> Self::Value { Ok(YamlOwned::Representation( - temporal.as_ref().to_string(), + temporal.as_ref().trim().to_string(), ScalarStyle::Plain, None, )) diff --git a/duper_website/src/visitor/toml.rs b/duper_website/src/visitor/toml.rs index fac74be..565a8f8 100644 --- a/duper_website/src/visitor/toml.rs +++ b/duper_website/src/visitor/toml.rs @@ -74,43 +74,46 @@ impl DuperVisitor for TomlVisitor { fn visit_temporal<'a>(&mut self, temporal: &DuperTemporal<'a>) -> Self::Value { match temporal { DuperTemporal::Instant { inner } => { - let datetime_str = Instant::from(inner.as_ref()).to_string(); + let datetime_str = Instant::from(super::clean_temporal(inner.as_ref())).to_string(); Ok(Some(Value::Datetime( Datetime::from_str(&datetime_str).map_err(|err| err.to_string())?, ))) } DuperTemporal::PlainDateTime { inner } => { - let datetime_str = PlainDateTime::from(inner.as_ref()).to_string(); + let datetime_str = + PlainDateTime::from(super::clean_temporal(inner.as_ref())).to_string(); Ok(Some(Value::Datetime( Datetime::from_str(&datetime_str).map_err(|err| err.to_string())?, ))) } DuperTemporal::PlainDate { inner } => { - let datetime_str = PlainDate::from(inner.as_ref()).to_string(); + let datetime_str = + PlainDate::from(super::clean_temporal(inner.as_ref())).to_string(); Ok(Some(Value::Datetime( Datetime::from_str(&datetime_str).map_err(|err| err.to_string())?, ))) } DuperTemporal::PlainTime { inner } => { - let datetime_str = PlainTime::from(inner.as_ref()).to_string(); + let datetime_str = + PlainTime::from(super::clean_temporal(inner.as_ref())).to_string(); Ok(Some(Value::Datetime( Datetime::from_str(&datetime_str).map_err(|err| err.to_string())?, ))) } DuperTemporal::ZonedDateTime { inner } => { - Ok(Some(Value::String(inner.as_ref().to_string()))) + Ok(Some(Value::String(inner.as_ref().trim().to_string()))) } DuperTemporal::PlainYearMonth { inner } => { - Ok(Some(Value::String(inner.as_ref().to_string()))) + Ok(Some(Value::String(inner.as_ref().trim().to_string()))) } DuperTemporal::PlainMonthDay { inner } => { - Ok(Some(Value::String(inner.as_ref().to_string()))) + Ok(Some(Value::String(inner.as_ref().trim().to_string()))) } DuperTemporal::Duration { inner } => { - Ok(Some(Value::String(inner.as_ref().to_string()))) + Ok(Some(Value::String(inner.as_ref().trim().to_string()))) } DuperTemporal::Unspecified { inner, .. } => { - Ok(Some(Value::String(inner.as_ref().to_string()))) + Ok(Some(Value::String(inner.as_ref().trim().to_string()))) } } } diff --git a/duper_zed/Cargo.toml b/duper_zed/Cargo.toml index 03efe01..81cc44e 100644 --- a/duper_zed/Cargo.toml +++ b/duper_zed/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "duper_zed" description = "Duper support for Zed" +version = "0.0.1" edition = "2024" rust-version = "1.88" license = "MIT" diff --git a/flake.nix b/flake.nix index 2af9609..3397888 100644 --- a/flake.nix +++ b/flake.nix @@ -43,17 +43,18 @@ } ); - cargo-rail = craneLib.buildPackage (finalAttrs: { + cargo-rail-version = "0.8.1"; + cargo-rail = craneLib.buildPackage { pname = "cargo-rail"; - version = "0.8.1"; + version = cargo-rail-version; src = pkgs.fetchFromGitHub { owner = "loadingalias"; repo = "cargo-rail"; - tag = "v${finalAttrs.version}"; + tag = "v${cargo-rail-version}"; hash = "sha256-GlApp4rJ/X5lSD2c3KJ5ll0ZBXEIY3DbWwMM1O/ryXw="; }; doCheck = false; - }); + }; src = lib.fileset.toSource { root = ./.; diff --git a/tree-sitter-duper/package.json b/tree-sitter-duper/package.json index 3601226..9024cd1 100644 --- a/tree-sitter-duper/package.json +++ b/tree-sitter-duper/package.json @@ -1,5 +1,5 @@ { - "name": "tree-sitter-duper", + "name": "@duper-js/tree-sitter-duper", "version": "0.1.3", "description": "The format that's super!", "repository": "https://github.com/epiceric/duper",