use std::collections::HashMap;
use std::sync::LazyLock;
use serde::Deserialize;
use smol_str::SmolStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArgKind {
Brace,
Bracket,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArgSpec {
pub required: bool,
pub kind: ArgKind,
pub prose: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CommandSig {
pub args: Vec<ArgSpec>,
pub sectioning: Option<u8>,
pub verbatim: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvironmentSig {
pub args: Vec<ArgSpec>,
pub verbatim_body: bool,
pub math: bool,
pub reflow: bool,
}
#[derive(Debug, Default)]
pub struct SignatureDb {
commands: HashMap<SmolStr, CommandSig>,
environments: HashMap<SmolStr, EnvironmentSig>,
}
impl SignatureDb {
pub fn command(&self, name: &str) -> Option<&CommandSig> {
self.commands.get(name)
}
pub fn environment(&self, name: &str) -> Option<&EnvironmentSig> {
self.environments.get(name)
}
pub fn insert_command(&mut self, name: impl Into<SmolStr>, sig: CommandSig) {
self.commands.insert(name.into(), sig);
}
pub fn insert_environment(&mut self, name: impl Into<SmolStr>, sig: EnvironmentSig) {
self.environments.insert(name.into(), sig);
}
}
#[derive(Debug, Clone, Copy)]
pub struct Signatures<'a> {
user: &'a SignatureDb,
}
impl<'a> Signatures<'a> {
pub fn new(user: &'a SignatureDb) -> Self {
Self { user }
}
pub fn command(&self, name: &str) -> Option<&'a CommandSig> {
self.user.command(name).or_else(|| builtin().command(name))
}
pub fn environment(&self, name: &str) -> Option<&'a EnvironmentSig> {
self.user
.environment(name)
.or_else(|| builtin().environment(name))
}
}
const SIGNATURES_JSON: &str = include_str!("../../data/signatures.json");
static DB: LazyLock<SignatureDb> =
LazyLock::new(|| parse(SIGNATURES_JSON).expect("bundled data/signatures.json must be valid"));
pub fn builtin() -> &'static SignatureDb {
&DB
}
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
enum RawArgKind {
Req,
Opt,
}
impl RawArgKind {
fn required(self) -> bool {
matches!(self, RawArgKind::Req)
}
fn kind(self) -> ArgKind {
match self {
RawArgKind::Req => ArgKind::Brace,
RawArgKind::Opt => ArgKind::Bracket,
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RawArg {
Short(RawArgKind),
Full {
kind: RawArgKind,
#[serde(default)]
prose: bool,
},
}
impl From<RawArg> for ArgSpec {
fn from(raw: RawArg) -> Self {
match raw {
RawArg::Short(kind) => ArgSpec {
required: kind.required(),
kind: kind.kind(),
prose: false,
},
RawArg::Full { kind, prose } => ArgSpec {
required: kind.required(),
kind: kind.kind(),
prose,
},
}
}
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RawCommand {
#[serde(default)]
args: Vec<RawArg>,
#[serde(default)]
sectioning: Option<u8>,
#[serde(default)]
verbatim: bool,
}
impl From<RawCommand> for CommandSig {
fn from(raw: RawCommand) -> Self {
CommandSig {
args: raw.args.into_iter().map(ArgSpec::from).collect(),
sectioning: raw.sectioning,
verbatim: raw.verbatim,
}
}
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RawEnvironment {
#[serde(default)]
args: Vec<RawArg>,
#[serde(default, rename = "verbatimBody")]
verbatim_body: bool,
#[serde(default)]
math: bool,
}
impl From<RawEnvironment> for EnvironmentSig {
fn from(raw: RawEnvironment) -> Self {
EnvironmentSig {
args: raw.args.into_iter().map(ArgSpec::from).collect(),
verbatim_body: raw.verbatim_body,
math: raw.math,
reflow: !(raw.verbatim_body || raw.math),
}
}
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RawDb {
#[serde(default)]
commands: HashMap<String, RawCommand>,
#[serde(default)]
environments: HashMap<String, RawEnvironment>,
}
fn parse(json: &str) -> serde_json::Result<SignatureDb> {
let raw: RawDb = serde_json::from_str(json)?;
Ok(SignatureDb {
commands: raw
.commands
.into_iter()
.map(|(name, sig)| (SmolStr::new(name), sig.into()))
.collect(),
environments: raw
.environments
.into_iter()
.map(|(name, sig)| (SmolStr::new(name), sig.into()))
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bundled_json_loads() {
let db = builtin();
assert!(db.command("section").is_some());
assert!(db.environment("tabular").is_some());
}
#[test]
fn loads_and_resolves_known_commands() {
let db = builtin();
assert_eq!(db.command("frac").map(|c| c.args.len()), Some(2));
assert!(db.command("frac").unwrap().args.iter().all(|a| a.required));
}
#[test]
fn optional_then_mandatory_order_preserved() {
let args = &builtin().command("includegraphics").unwrap().args;
assert_eq!(args.len(), 2);
assert_eq!(args[0].kind, ArgKind::Bracket);
assert!(!args[0].required);
assert_eq!(args[1].kind, ArgKind::Brace);
assert!(args[1].required);
}
#[test]
fn mixed_argument_order_round_trips() {
let args = &builtin().command("newcommand").unwrap().args;
let kinds: Vec<_> = args.iter().map(|a| a.kind).collect();
assert_eq!(
kinds,
vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
);
}
#[test]
fn sectioning_levels_assigned() {
let db = builtin();
assert_eq!(db.command("part").unwrap().sectioning, Some(0));
assert_eq!(db.command("section").unwrap().sectioning, Some(2));
assert_eq!(db.command("subsubsection").unwrap().sectioning, Some(4));
assert_eq!(db.command("section").unwrap().args.len(), 2);
assert!(db.command("textbf").unwrap().sectioning.is_none());
}
#[test]
fn verbatim_commands_flagged() {
assert!(builtin().command("verb").unwrap().verbatim);
assert!(builtin().command("lstinline").unwrap().verbatim);
assert!(!builtin().command("textbf").unwrap().verbatim);
}
#[test]
fn prose_arg_parses_from_both_forms() {
let db = parse(
r#"{ "commands": {
"short": { "args": ["req"] },
"full": { "args": ["opt", { "kind": "req", "prose": true }] }
} }"#,
)
.expect("valid prose schema");
let short = &db.command("short").unwrap().args;
assert!(!short[0].prose);
let full = &db.command("full").unwrap().args;
assert_eq!(full[0].kind, ArgKind::Bracket);
assert!(!full[0].prose); assert_eq!(full[1].kind, ArgKind::Brace);
assert!(full[1].prose);
}
#[test]
fn bundled_prose_args_flagged() {
let footnote = &builtin().command("footnote").unwrap().args;
assert!(footnote.iter().any(|a| a.prose));
let label = &builtin().command("label").unwrap().args;
assert!(label.iter().all(|a| !a.prose));
}
#[test]
fn environment_argument_shapes() {
let db = builtin();
let tabular = db.environment("tabular").unwrap();
assert_eq!(tabular.args.len(), 2);
assert_eq!(tabular.args[0].kind, ArgKind::Bracket); assert_eq!(tabular.args[1].kind, ArgKind::Brace); assert!(db.environment("verbatim").unwrap().args.is_empty());
}
#[test]
fn environment_flags_and_derived_reflow() {
let db = builtin();
let lstlisting = db.environment("lstlisting").unwrap();
assert!(lstlisting.verbatim_body);
assert!(!lstlisting.reflow);
let equation = db.environment("equation").unwrap();
assert!(equation.math);
assert!(!equation.reflow);
let tabular = db.environment("tabular").unwrap();
assert!(!tabular.verbatim_body);
assert!(!tabular.math);
assert!(tabular.reflow);
}
#[test]
fn unknown_names_resolve_to_none() {
let db = builtin();
assert!(db.command("definitelynotacommand").is_none());
assert!(db.environment("definitelynotanenv").is_none());
}
#[test]
fn rejects_unknown_fields() {
let err = parse(r#"{ "commands": { "x": { "sektioning": 2 } } }"#);
assert!(err.is_err());
}
#[test]
fn empty_document_is_valid() {
let db = parse("{}").expect("empty object is valid");
assert!(db.command("anything").is_none());
}
}