Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Improvements and bug fixes

* `input_code_editor()` now supports `language = "ggsql"` for syntax highlighting of ggsql queries. (#1291)

* Fixed `toolbar_input_button()` alignment and spacing issues. (#1290)

* The brand.yml example app (`shiny::runExample("brand.yml", package = "bslib")`) now uses `brand_pluck()` and `brand_has()` from `{brand.yml}`. (#1288)
Expand Down
Binary file modified R/sysdata.rda
Binary file not shown.
2 changes: 1 addition & 1 deletion R/versions.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ version_bs4 <- "4.6.0"
version_bs3 <- "3.4.1"
version_accessibility <- "1.0.6"
version_prism_code_editor <- "4.2.0"
code_editor_bundled_languages <- c("r", "python", "julia", "sql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff")
code_editor_bundled_languages <- c("r", "python", "julia", "sql", "ggsql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff")
79 changes: 79 additions & 0 deletions inst/lib/prism-code-editor/prism/languages/ggsql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// ggsql.js — ggsql language for prism-code-editor
//
// AUTO-GENERATED by tools/build_ggsql_grammar.R from the TextMate grammar at:
// https://github.com/posit-dev/ggsql/blob/main/ggsql-vscode/syntaxes/ggsql.tmLanguage.json
// Do not edit by hand.
//
// NOTE: The import path below is a content-hashed internal module of
// prism-code-editor. If bslib updates prism-code-editor, this hash may change.
// Re-run this script to regenerate.

import "./sql.js";
import { l as languages } from "../../index-C1_GGQ8y.js";

var sql = languages.sql;
var ggsql = {};

// Copy SQL tokens
Object.keys(sql).forEach(function(k) { ggsql[k] = sql[k]; });

// ggsql clause keywords
ggsql["ggsql-keyword"] = {
pattern: /\b(?:VISUALISE|VISUALIZE|DRAW|SCALE|FACET|PROJECT|LABEL|THEME|MAPPING|REMAPPING|SETTING|FILTER|FROM|ORDER|BY|PARTITION|RENAMING|AS|TO|VIA)\b/i,
alias: "keyword",
};

// Geom types
ggsql["ggsql-geom"] = {
pattern: /\b(?:point|line|path|bar|col|area|tile|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|label|segment|arrow|rule|linear|errorbar)\b/,
alias: "builtin",
};

// Scale type modifiers
ggsql["ggsql-scale-type"] = {
pattern: /\b(?:CONTINUOUS|DISCRETE|BINNED|ORDINAL|IDENTITY)\b/i,
alias: "builtin",
};

// Aesthetic names
ggsql["ggsql-aesthetic"] = {
pattern: /\b(?:x|y|xmin|xmax|ymin|ymax|xend|yend|weight|color|colour|fill|stroke|opacity|size|shape|linetype|linewidth|width|height|label|family|fontface|hjust|vjust|panel|row|column)\b/,
alias: "attr-name",
};

// Theme names
ggsql["ggsql-theme"] = {
pattern: /\b(?:minimal|classic|gray|grey|bw|dark|light|void)\b/,
alias: "class-name",
};

// Project types
ggsql["ggsql-project"] = {
pattern: /\b(?:cartesian|polar|flip|fixed|trans|map|quickmap)\b/,
alias: "class-name",
};

// Fat arrow operator
ggsql["ggsql-arrow"] = {
pattern: /=>/,
alias: "operator",
};

// SQL functions (aggregate, window, datetime, string, math, conversion, conditional, JSON, list)
ggsql["function"] = /\b(?:count|sum|avg|min|max|stddev|variance|array_agg|string_agg|group_concat|row_number|rank|dense_rank|ntile|lag|lead|first_value|last_value|nth_value|cume_dist|percent_rank|date_trunc|date_part|datepart|datename|dateadd|datediff|extract|now|current_date|current_time|current_timestamp|getdate|getutcdate|strftime|strptime|make_date|make_time|make_timestamp|concat|substring|substr|left|right|length|len|char_length|lower|upper|trim|ltrim|rtrim|replace|reverse|repeat|lpad|rpad|split_part|string_split|format|printf|regexp_replace|regexp_extract|regexp_matches|abs|ceil|ceiling|floor|round|trunc|truncate|mod|power|sqrt|exp|ln|log|log10|log2|sign|sin|cos|tan|asin|acos|atan|atan2|pi|degrees|radians|random|rand|cast|convert|coalesce|nullif|ifnull|isnull|nvl|try_cast|typeof|if|iff|iif|greatest|least|decode|json|json_extract|json_extract_path|json_extract_string|json_value|json_query|json_object|json_array|json_array_length|to_json|from_json|list|list_value|list_aggregate|array_length|unnest|generate_series|range)(?=\s*\()/i;

// Reorder: ggsql tokens before generic SQL keyword/boolean
var ordered = {};
["comment", "variable", "string", "identifier"].forEach(function(k) {
if (k in ggsql) ordered[k] = ggsql[k];
});
["ggsql-keyword", "ggsql-geom", "ggsql-scale-type", "ggsql-aesthetic",
"ggsql-theme", "ggsql-project", "ggsql-arrow"].forEach(function(k) {
if (k in ggsql) ordered[k] = ggsql[k];
});
Object.keys(ggsql).forEach(function(k) {
if (!(k in ordered)) ordered[k] = ggsql[k];
});

languages.ggsql = ordered;

6 changes: 3 additions & 3 deletions tests/testthat/_snaps/input-code-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
input_code_editor("test", language = "fortran")
Condition
Error in `input_code_editor()`:
! `language` must be one of "r", "python", "julia", "sql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff", "md", "html", "plain", "plaintext", "text", or "txt", not "fortran".
! `language` must be one of "r", "python", "julia", "sql", "ggsql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff", "md", "html", "plain", "plaintext", "text", or "txt", not "fortran".

# input_code_editor validates theme names

Expand All @@ -33,15 +33,15 @@
input_code_editor("test", language = "fortran")
Condition
Error in `input_code_editor()`:
! `language` must be one of "r", "python", "julia", "sql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff", "md", "html", "plain", "plaintext", "text", or "txt", not "fortran".
! `language` must be one of "r", "python", "julia", "sql", "ggsql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff", "md", "html", "plain", "plaintext", "text", or "txt", not "fortran".

# update_code_editor validates inputs

Code
update_code_editor("test", language = "fortran", session = NULL)
Condition
Error in `update_code_editor()`:
! `language` must be one of "r", "python", "julia", "sql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff", "md", "html", "plain", "plaintext", "text", or "txt", not "fortran".
! `language` must be one of "r", "python", "julia", "sql", "ggsql", "javascript", "typescript", "markup", "css", "scss", "sass", "json", "markdown", "yaml", "xml", "toml", "ini", "bash", "docker", "latex", "cpp", "rust", "diff", "md", "html", "plain", "plaintext", "text", or "txt", not "fortran".

---

Expand Down
258 changes: 258 additions & 0 deletions tools/build_ggsql_grammar.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
#!/usr/bin/env Rscript
#
# Build the ggsql Prism grammar file from the TextMate grammar source.
#
# Usage:
# Rscript tools/build_ggsql_grammar.R
#
# This fetches the ggsql TextMate grammar from posit-dev/ggsql on GitHub,
# extracts token patterns, and generates a Prism-compatible grammar file
# that extends the built-in SQL grammar.
#
# The output is written to:
# inst/lib/prism-code-editor/prism/languages/ggsql.js

TMGRAMMAR_URL <- "https://raw.githubusercontent.com/posit-dev/ggsql/main/ggsql-vscode/syntaxes/ggsql.tmLanguage.json"

# Paths relative to the bslib package root
LANGUAGES_DIR <- "inst/lib/prism-code-editor/prism/languages"
SQL_JS <- file.path(LANGUAGES_DIR, "sql.js")
OUTPUT_JS <- file.path(LANGUAGES_DIR, "ggsql.js")

# ---- Helpers ----

# Extract word alternatives from a TextMate match pattern like
# "\\b(word1|word2|word3)\\b" or "\\b(?i:word1|word2)\\b"
extract_words <- function(match_pattern) {
# Try non-capture group with flags first: \b(?i:word1|word2)\b
m <- regmatches(
match_pattern,
regexec("\\\\b\\(\\?[a-z]*:([^)]+)\\)\\\\b", match_pattern)
)[[1]]
if (length(m) == 2) {
return(strsplit(m[2], "\\|")[[1]])
}

# Try capture group: \b(word1|word2)\b
m <- regmatches(
match_pattern,
regexec("\\\\b\\(([^)]+)\\)\\\\b", match_pattern)
)[[1]]
if (length(m) == 2) {
return(strsplit(m[2], "\\|")[[1]])
}

NULL
}

# Extract word alternatives from a function pattern like
# "(?i)\\b(func1|func2)\\b\\s*\\("
extract_function_words <- function(match_pattern) {
m <- regmatches(
match_pattern,
regexec("\\\\b\\(([^)]+)\\)\\\\b\\\\s\\*\\\\\\(", match_pattern)
)[[1]]
if (length(m) == 2) {
return(strsplit(m[2], "\\|")[[1]])
}
NULL
}

# Format a word list as a JS regex alternation: /\b(?:word1|word2)\b/flags
format_word_regex <- function(words, flags = "") {
alts <- paste(words, collapse = "|")
sprintf("/\\b(?:%s)\\b/%s", alts, flags)
}

# Format function words with lookahead: /\b(?:func1|func2)(?=\s*\()/flags
format_function_regex <- function(words, flags = "i") {
alts <- paste(words, collapse = "|")
sprintf("/\\b(?:%s)(?=\\s*\\()/%s", alts, flags)
}

# Extract the content-hashed import path from sql.js
extract_import_path <- function(sql_js_path) {
lines <- readLines(sql_js_path, n = 1)
m <- regmatches(lines, regexec('"([^"]+index-[^"]+\\.js)"', lines))[[1]]
if (length(m) < 2) {
stop("Could not extract import path from ", sql_js_path)
}
m[2]
}

# ---- Main ----

main <- function() {
if (!file.exists(SQL_JS)) {
stop("sql.js not found at ", SQL_JS, ". Run from the bslib package root.")
}

message("Fetching TextMate grammar from: ", TMGRAMMAR_URL)
grammar <- jsonlite::fromJSON(TMGRAMMAR_URL, simplifyDataFrame = FALSE)
repo <- grammar$repository

import_path <- extract_import_path(SQL_JS)
message("Using import path: ", import_path)

# -- Extract ggsql clause keywords --
# Collect from: begin captures of clause patterns + keyword.other matches
clause_keywords <- character(0)

# Clause names come from the `begin` regex of each *-clause pattern
clause_names <- grep("-clause$", names(repo), value = TRUE)
for (clause_name in clause_names) {
clause <- repo[[clause_name]]
if (!is.null(clause$begin)) {
words <- extract_words(clause$begin)
if (!is.null(words)) clause_keywords <- c(clause_keywords, words)
}
}

# Also collect keyword.other.ggsql from all clause patterns and common-clause-patterns
keyword_sources <- c("common-clause-patterns", clause_names)
for (src in keyword_sources) {
for (p in repo[[src]]$patterns) {
if (identical(p$name, "keyword.other.ggsql")) {
words <- extract_words(p$match)
if (!is.null(words)) clause_keywords <- c(clause_keywords, words)
}
}
}
clause_keywords <- unique(clause_keywords)

# -- Extract geom types --
geoms <- NULL
for (p in repo[["draw-clause"]]$patterns) {
if (identical(p$name, "support.type.geom.ggsql")) {
geoms <- extract_words(p$match)
}
}

# -- Extract scale type modifiers --
scale_types <- NULL
for (p in repo[["scale-clause"]]$patterns) {
if (identical(p$name, "keyword.control.scale-modifier.ggsql")) {
scale_types <- extract_words(p$match)
}
}

# -- Extract aesthetic names --
aesthetics <- NULL
for (p in repo[["aesthetics"]]$patterns) {
if (identical(p$name, "support.type.aesthetic.ggsql")) {
aesthetics <- extract_words(p$match)
}
}

# -- Extract theme names --
themes <- NULL
for (p in repo[["theme-clause"]]$patterns) {
if (identical(p$name, "support.type.theme.ggsql")) {
themes <- extract_words(p$match)
}
}

# -- Extract project types --
projects <- NULL
for (p in repo[["project-clause"]]$patterns) {
if (identical(p$name, "support.type.project.ggsql")) {
projects <- extract_words(p$match)
}
}

# -- Extract SQL function names --
all_functions <- character(0)
for (p in repo[["sql-functions"]]$patterns) {
words <- extract_function_words(p$match)
if (!is.null(words)) all_functions <- c(all_functions, words)
}
all_functions <- unique(all_functions)

# -- Generate the JS file --
js <- c(
"// ggsql.js — ggsql language for prism-code-editor",
"//",
"// AUTO-GENERATED by tools/build_ggsql_grammar.R from the TextMate grammar at:",
"// https://github.com/posit-dev/ggsql/blob/main/ggsql-vscode/syntaxes/ggsql.tmLanguage.json",
"// Do not edit by hand.",
"//",
"// NOTE: The import path below is a content-hashed internal module of",
"// prism-code-editor. If bslib updates prism-code-editor, this hash may change.",
"// Re-run this script to regenerate.",
"",
'import "./sql.js";',
sprintf('import { l as languages } from "%s";', import_path),
"",
"var sql = languages.sql;",
"var ggsql = {};",
"",
"// Copy SQL tokens",
'Object.keys(sql).forEach(function(k) { ggsql[k] = sql[k]; });',
"",
"// ggsql clause keywords",
sprintf('ggsql["ggsql-keyword"] = {'),
sprintf(" pattern: %s,", format_word_regex(clause_keywords, "i")),
' alias: "keyword",',
"};",
"",
"// Geom types",
sprintf('ggsql["ggsql-geom"] = {'),
sprintf(" pattern: %s,", format_word_regex(geoms)),
' alias: "builtin",',
"};",
"",
"// Scale type modifiers",
sprintf('ggsql["ggsql-scale-type"] = {'),
sprintf(" pattern: %s,", format_word_regex(scale_types, "i")),
' alias: "builtin",',
"};",
"",
"// Aesthetic names",
sprintf('ggsql["ggsql-aesthetic"] = {'),
sprintf(" pattern: %s,", format_word_regex(aesthetics)),
' alias: "attr-name",',
"};",
"",
"// Theme names",
sprintf('ggsql["ggsql-theme"] = {'),
sprintf(" pattern: %s,", format_word_regex(themes)),
' alias: "class-name",',
"};",
"",
"// Project types",
sprintf('ggsql["ggsql-project"] = {'),
sprintf(" pattern: %s,", format_word_regex(projects)),
' alias: "class-name",',
"};",
"",
"// Fat arrow operator",
'ggsql["ggsql-arrow"] = {',
" pattern: /=>/,",
' alias: "operator",',
"};",
"",
"// SQL functions (aggregate, window, datetime, string, math, conversion, conditional, JSON, list)",
sprintf('ggsql["function"] = %s;', format_function_regex(all_functions)),
"",
"// Reorder: ggsql tokens before generic SQL keyword/boolean",
"var ordered = {};",
'["comment", "variable", "string", "identifier"].forEach(function(k) {',
" if (k in ggsql) ordered[k] = ggsql[k];",
"});",
'["ggsql-keyword", "ggsql-geom", "ggsql-scale-type", "ggsql-aesthetic",',
' "ggsql-theme", "ggsql-project", "ggsql-arrow"].forEach(function(k) {',
" if (k in ggsql) ordered[k] = ggsql[k];",
"});",
"Object.keys(ggsql).forEach(function(k) {",
" if (!(k in ordered)) ordered[k] = ggsql[k];",
"});",
"",
"languages.ggsql = ordered;",
""
)

writeLines(js, OUTPUT_JS)
message("Generated: ", OUTPUT_JS)
}

main()
3 changes: 2 additions & 1 deletion tools/main.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ lapply(
"download_preset_fonts.R",
"update_gfont_info.R",
"expand_variables_article_template.R",
"compile_component_sass.R"
"compile_component_sass.R",
"build_ggsql_grammar.R"
),
function(file) {
message("Updating: ", file)
Expand Down
1 change: 1 addition & 0 deletions tools/yarn_install.R
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ with_dir("inst/lib", {
"python",
"julia",
"sql",
"ggsql",
# Web/Frontend
"javascript",
"typescript",
Expand Down
Loading