diff --git a/src/parser/builder.rs b/src/parser/builder.rs index da5c0d00..6d8ebbfa 100644 --- a/src/parser/builder.rs +++ b/src/parser/builder.rs @@ -36,10 +36,40 @@ fn extract_name_value_nodes<'a>(node: &'a Node<'a>, context: &str) -> Result<(No Ok((name_node, value_node)) } -/// Parse a string node, removing quotes +/// Parse a string node, removing quotes and processing escape sequences fn parse_string_node(node: &Node, source: &SourceTree) -> String { let text = source.get_text(node); - text.trim_matches(|c| c == '\'' || c == '"').to_string() + let unquoted = text.trim_matches(|c| c == '\'' || c == '"'); + process_escape_sequences(unquoted) +} + +/// Process escape sequences in a string (e.g., \n, \t, \\, \') +fn process_escape_sequences(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + let mut chars = s.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next() { + Some('n') => result.push('\n'), + Some('t') => result.push('\t'), + Some('r') => result.push('\r'), + Some('\\') => result.push('\\'), + Some('\'') => result.push('\''), + Some('"') => result.push('"'), + Some(other) => { + // Unknown escape sequence - keep as-is + result.push('\\'); + result.push(other); + } + None => result.push('\\'), // Trailing backslash + } + } else { + result.push(c); + } + } + + result } /// Parse a number node into f64 diff --git a/src/writer/vegalite/mod.rs b/src/writer/vegalite/mod.rs index aa404d8a..8c853312 100644 --- a/src/writer/vegalite/mod.rs +++ b/src/writer/vegalite/mod.rs @@ -355,6 +355,65 @@ impl VegaLiteWriter { schema: "https://vega.github.io/schema/vega-lite/v6.json".to_string(), } } + + /// Build default Vega-Lite config matching ggplot2's theme_gray() + /// + /// Font sizes converted from ggplot2 points to pixels (1 pt ≈ 1.33 px at 96 DPI): + /// - axis.text: 8.8 pts (rel(0.8) × 11) → 12 px + /// - axis.title: 11 pts → 15 px + /// - legend.text: 8.8 pts → 12 px + /// - legend.title: 11 pts → 15 px + /// - plot.title: 13.2 pts (rel(1.2) × 11) → 18 px + /// - tick size: ~2.75 pts → 4 px + fn default_theme_config(&self) -> Value { + json!({ + "view": { + "stroke": null, + "fill": "#EBEBEB" + }, + "axis": { + "domain": false, + "grid": true, + "gridColor": "#FFFFFF", + "gridWidth": 1, + "tickColor": "#333333", + "tickSize": 4, + "labelColor": "#4D4D4D", + "labelFontSize": 12, + "titleColor": "#000000", + "titleFontSize": 15, + "titleFontWeight": "normal", + "titlePadding": 10 + }, + "legend": { + "labelColor": "#4D4D4D", + "labelFontSize": 12, + "titleColor": "#000000", + "titleFontSize": 15, + "titleFontWeight": "normal", + "titlePadding": 8, + "rowPadding": 6 + }, + "title": { + "color": "#000000", + "fontSize": 18, + "fontWeight": "normal", + "subtitleColor": "#4D4D4D", + "subtitleFontSize": 15, + "subtitleFontWeight": "normal", + "anchor": "start", + "frame": "group", + "offset": 10 + }, + "header": { + "labelColor": "#000000", + "labelFontSize": 15, + "labelFontWeight": "normal", + "labelPadding": 5, + "title": null + } + }) + } } impl Default for VegaLiteWriter { @@ -439,7 +498,10 @@ impl Writer for VegaLiteWriter { apply_faceting(&mut vl_spec, facet, facet_df); } - // 11. Serialize + // 11. Add default theme config (ggplot2-like gray theme) + vl_spec["config"] = self.default_theme_config(); + + // 12. Serialize serde_json::to_string_pretty(&vl_spec).map_err(|e| { GgsqlError::WriterError(format!("Failed to serialize Vega-Lite JSON: {}", e)) }) diff --git a/tree-sitter-ggsql/grammar.js b/tree-sitter-ggsql/grammar.js index ef793a21..bceee311 100644 --- a/tree-sitter-ggsql/grammar.js +++ b/tree-sitter-ggsql/grammar.js @@ -870,7 +870,7 @@ module.exports = grammar({ ) )), - string: $ => seq("'", repeat(choice(/[^'\\]/, seq('\\', /.*/))), "'"), + string: $ => seq("'", repeat(choice(/[^'\\]/, /\\./)), "'"), boolean: $ => choice('true', 'false'),