use crate::dialect::{Dialect, Rule};
use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub struct Preset {
pub detect: Detect,
pub dialect: Dialect,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Detect {
#[serde(default)]
pub source_contains: Vec<String>,
#[serde(default)]
pub sibling_files: Vec<String>,
}
impl Detect {
fn matches(&self, source: &str, dir: &Path) -> bool {
self.source_contains.iter().any(|s| source.contains(s))
|| self.sibling_files.iter().any(|f| dir.join(f).is_file())
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct PresetFile {
name: String,
#[serde(default)]
detect: Detect,
#[serde(default)]
library: LibrarySpec,
#[serde(default)]
defines: Vec<String>,
#[serde(default)]
discover_defines: bool,
prelude: Option<String>,
prelude_vertex: Option<String>,
prelude_fragment: Option<String>,
epilogue: Option<String>,
#[serde(default)]
deck: bool,
#[serde(default, rename = "expand")]
expand: Vec<ExpandRuleFile>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct LibrarySpec {
#[serde(default)]
vertex: Vec<String>,
#[serde(default)]
fragment: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct ExpandRuleFile {
pragma: OneOrMany,
verb: Option<String>,
#[serde(default)]
args: Vec<String>,
emit: Option<String>,
vertex: Option<String>,
fragment: Option<String>,
compute: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
impl OneOrMany {
fn into_vec(self) -> Vec<String> {
match self {
OneOrMany::One(s) => vec![s],
OneOrMany::Many(v) => v,
}
}
}
pub fn parse(text: &str) -> Result<Preset, toml::de::Error> {
let pf: PresetFile = toml::from_str(text)?;
Ok(build(pf))
}
fn build(pf: PresetFile) -> Preset {
let mut rules = Vec::new();
for e in pf.expand {
for ns in e.pragma.clone().into_vec() {
rules.push(Rule {
ns,
verb: e.verb.clone(),
args: e.args.clone(),
vertex: e.vertex.clone(),
fragment: e.fragment.clone(),
compute: e.compute.clone(),
any: e.emit.clone(),
});
}
}
let dialect = Dialect {
name: pf.name,
prelude_vertex: pf.prelude_vertex,
prelude_fragment: pf.prelude_fragment,
prelude_any: pf.prelude,
prelude_extra: None,
epilogue: pf.epilogue,
prelude_files_vertex: pf.library.vertex,
prelude_files_fragment: pf.library.fragment,
define_names: pf.defines,
discover_defines: pf.discover_defines,
rules,
deck: pf.deck,
};
Preset {
detect: pf.detect,
dialect,
}
}
pub fn bundled() -> &'static [Preset] {
static BUNDLED: OnceLock<Vec<Preset>> = OnceLock::new();
BUNDLED.get_or_init(|| {
[
include_str!("../presets/maplibre.toml"),
include_str!("../presets/shadertoy.toml"),
include_str!("../presets/deck.toml"),
]
.iter()
.filter_map(|t| parse(t).ok())
.collect()
})
}
pub fn deck_prelude() -> &'static str {
static P: OnceLock<String> = OnceLock::new();
P.get_or_init(|| {
bundled()
.iter()
.find(|p| p.dialect.name == "deck")
.and_then(|p| p.dialect.prelude_any.clone())
.unwrap_or_default()
})
}
pub fn project(dir: &Path) -> Vec<Preset> {
let Some(path) = find_up(dir, "glslint.toml") else {
return Vec::new();
};
match std::fs::read_to_string(&path)
.ok()
.and_then(|t| parse(&t).ok())
{
Some(p) => vec![p],
None => Vec::new(),
}
}
pub fn by_name(name: &str, dir: &Path) -> Option<Dialect> {
if name == "none" || name == "raw" {
return Some(Dialect {
name: "none".into(),
deck: false,
..Default::default()
});
}
project(dir)
.into_iter()
.find(|p| p.dialect.name == name)
.map(|p| p.dialect)
.or_else(|| {
bundled()
.iter()
.find(|p| p.dialect.name == name)
.map(|p| p.dialect.clone())
})
}
pub fn detect(source: &str, dir: &Path) -> Option<Dialect> {
for p in project(dir) {
if p.detect.matches(source, dir) {
return Some(p.dialect);
}
}
bundled()
.iter()
.find(|p| p.detect.matches(source, dir))
.map(|p| p.dialect.clone())
}
fn find_up(start: &Path, name: &str) -> Option<PathBuf> {
let mut dir = Some(start);
while let Some(d) = dir {
let candidate = d.join(name);
if candidate.is_file() {
return Some(candidate);
}
if d.join(".git").exists() {
break;
}
dir = d.parent();
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::assemble::Stage;
#[test]
fn bundled_presets_parse() {
let names: Vec<&str> = bundled().iter().map(|p| p.dialect.name.as_str()).collect();
assert!(names.contains(&"maplibre"), "got {names:?}");
assert!(names.contains(&"shadertoy"), "got {names:?}");
assert!(names.contains(&"deck"), "got {names:?}");
}
#[test]
fn deck_preset_provides_stub_prelude_and_detection() {
let bare = Path::new("/nonexistent-glslint");
let d = by_name("deck", bare).unwrap();
assert!(d.deck);
assert!(deck_prelude().contains("project_position_to_clipspace"));
assert_eq!(
detect(
"void main() { gl_Position = project_position_to_clipspace(a, b, c); }",
bare
)
.map(|d| d.name),
Some("deck".into())
);
}
#[test]
fn maplibre_preset_carries_its_knowledge() {
let d = by_name("maplibre", Path::new("/nonexistent")).unwrap();
assert!(
d.rules
.iter()
.any(|r| r.ns == "maplibre" && r.verb.as_deref() == Some("define"))
);
assert!(d.rules.iter().any(|r| r.ns == "mapbox"));
assert!(
d.prelude_files_vertex
.iter()
.any(|f| f.contains("_projection_mercator"))
);
assert!(
d.define_names
.iter()
.any(|n| n == "NUM_ILLUMINATION_SOURCES")
);
let v = d
.expand_line("#pragma maplibre: define lowp float opacity", Stage::Vertex)
.unwrap();
assert!(v.iter().any(|l| l == "out lowp float opacity;"));
}
#[test]
fn detect_uses_source_and_sibling_signals() {
let bare = Path::new("/nonexistent-glslint");
assert_eq!(
detect("#pragma maplibre: define lowp float x", bare).map(|d| d.name),
Some("maplibre".into())
);
assert_eq!(
detect("void mainImage(out vec4 c, in vec2 p) {}", bare).map(|d| d.name),
Some("shadertoy".into())
);
assert_eq!(detect("void main(){}", bare).map(|d| d.name), None);
}
#[test]
fn one_or_many_accepts_both_forms() {
let one = parse("name='a'\n[[expand]]\npragma='x'\nemit='y'\n").unwrap();
let many = parse("name='b'\n[[expand]]\npragma=['x','z']\nemit='y'\n").unwrap();
assert_eq!(one.dialect.rules.len(), 1);
assert_eq!(many.dialect.rules.len(), 2);
}
}