From a422820ecfd69d387083e50f466360b2c5533481 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Wed, 18 Feb 2026 09:52:39 +0100 Subject: [PATCH 1/2] Allow newline in strings --- src/parser/builder.rs | 34 ++++++++++++++++++++++++++++++++-- tree-sitter-ggsql/grammar.js | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) 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/tree-sitter-ggsql/grammar.js b/tree-sitter-ggsql/grammar.js index 10067b54..c6c070a3 100644 --- a/tree-sitter-ggsql/grammar.js +++ b/tree-sitter-ggsql/grammar.js @@ -849,7 +849,7 @@ module.exports = grammar({ ) )), - string: $ => seq("'", repeat(choice(/[^'\\]/, seq('\\', /.*/))), "'"), + string: $ => seq("'", repeat(choice(/[^'\\]/, /\\./)), "'"), boolean: $ => choice('true', 'false'), From 0440a760af4bf42fba49a7b9171d6cd400efe740 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Wed, 18 Feb 2026 09:52:52 +0100 Subject: [PATCH 2/2] update theme to match ggplot2 --- src/writer/vegalite.rs | 95 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/src/writer/vegalite.rs b/src/writer/vegalite.rs index ec6b2950..a8bd7d36 100644 --- a/src/writer/vegalite.rs +++ b/src/writer/vegalite.rs @@ -1737,6 +1737,65 @@ impl VegaLiteWriter { Some(json!(details)) } } + + /// 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 Writer for VegaLiteWriter { @@ -1782,10 +1841,37 @@ impl Writer for VegaLiteWriter { vl_spec["width"] = json!("container"); vl_spec["height"] = json!("container"); - // Add title if present + // Add title and/or subtitle if present + // Split on newlines to support multi-line text in Vega-Lite + fn to_text_value(s: &str) -> Value { + if s.contains('\n') { + json!(s.lines().collect::>()) + } else { + json!(s) + } + } + if let Some(labels) = &spec.labels { - if let Some(title) = labels.labels.get("title") { - vl_spec["title"] = json!(title); + let title = labels.labels.get("title"); + let subtitle = labels.labels.get("subtitle"); + + match (title, subtitle) { + (Some(t), Some(s)) => { + vl_spec["title"] = json!({ + "text": to_text_value(t), + "subtitle": to_text_value(s) + }); + } + (Some(t), None) => { + vl_spec["title"] = to_text_value(t); + } + (None, Some(s)) => { + vl_spec["title"] = json!({ + "text": null, + "subtitle": to_text_value(s) + }); + } + (None, None) => {} } } @@ -2062,6 +2148,9 @@ impl Writer for VegaLiteWriter { } } + // Add default theme config (ggplot2-like gray theme) + vl_spec["config"] = self.default_theme_config(); + serde_json::to_string_pretty(&vl_spec).map_err(|e| { GgsqlError::WriterError(format!("Failed to serialize Vega-Lite JSON: {}", e)) })