texform-knowledge 0.1.0

LaTeX command and environment knowledge base for TeXForm (internal; use the texform crate)
Documentation
//! Shared spec types.
//!
//! This crate hosts:
//! - `PackageSpecs`: parsed YAML package specs (owned, merge-ready)
//! - builtin records generated at compile time
//! - active records assembled by the knowledge base at runtime

#[path = "specs_yaml.rs"]
mod specs_yaml;

use specs_yaml::{
    AllowedModeYaml, CharacterAttributesYaml, CharacterSpecYaml, CommandKindYaml, CommandSpecYaml,
    ContentModeYaml, DelimiterSpecYaml, EnvironmentSpecYaml, PackageSpecsYaml,
};

pub use texform_argspec::ContentMode;
pub use texform_argspec::{
    ArgForm, ArgSpec, ArgSpecParseError, DelimiterToken, OwnedArgSpec, ParsedArgSpec, ValueKind,
    parse_arg_specs,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandKind {
    Prefix,
    Infix,
    Declarative,
}

impl CommandKind {
    pub const fn label(&self) -> &'static str {
        match self {
            CommandKind::Prefix => "prefix",
            CommandKind::Infix => "infix",
            CommandKind::Declarative => "declarative",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllowedMode {
    Math,
    Text,
    Both,
}

impl AllowedMode {
    pub const fn as_str(self) -> &'static str {
        match self {
            AllowedMode::Math => "math",
            AllowedMode::Text => "text",
            AllowedMode::Both => "both",
        }
    }

    pub const fn union(self, other: Self) -> Self {
        match (self, other) {
            (AllowedMode::Both, _) | (_, AllowedMode::Both) => AllowedMode::Both,
            (AllowedMode::Math, AllowedMode::Math) => AllowedMode::Math,
            (AllowedMode::Text, AllowedMode::Text) => AllowedMode::Text,
            (AllowedMode::Math, AllowedMode::Text) | (AllowedMode::Text, AllowedMode::Math) => {
                AllowedMode::Both
            }
        }
    }

    pub const fn allows(self, mode: ContentMode) -> bool {
        match self {
            AllowedMode::Both => true,
            AllowedMode::Math => matches!(mode, ContentMode::Math),
            AllowedMode::Text => matches!(mode, ContentMode::Text),
        }
    }
}

impl std::fmt::Display for AllowedMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str((*self).as_str())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinCommandRecord {
    pub name: &'static str,
    pub kind: CommandKind,
    pub allowed_mode: AllowedMode,
    pub argspec: ParsedArgSpec,
    pub tags: &'static [&'static str],
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinEnvironmentRecord {
    pub name: &'static str,
    pub allowed_mode: AllowedMode,
    pub argspec: ParsedArgSpec,
    pub body_mode: ContentMode,
    pub tags: &'static [&'static str],
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinCharacterAttributes {
    pub mathvariant: Option<&'static str>,
    pub tex_class: Option<&'static str>,
    pub stretchy: Option<bool>,
    pub move_sup_sub: Option<bool>,
    pub large_op: Option<bool>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinCharacterRecord {
    pub name: &'static str,
    pub allowed_mode: AllowedMode,
    pub unicode_value: &'static str,
    pub attributes: BuiltinCharacterAttributes,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinDelimiterRecord {
    pub name: &'static str,
    pub is_control_sequence: bool,
    pub allowed_mode: AllowedMode,
    pub unicode_value: &'static str,
    pub attributes: BuiltinCharacterAttributes,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveCommandRecord {
    pub name: &'static str,
    pub kind: CommandKind,
    pub allowed_mode: AllowedMode,
    pub argspec: ParsedArgSpec,
    pub tags: &'static [&'static str],
    pub from_packages: &'static [&'static str],
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveEnvironmentRecord {
    pub name: &'static str,
    pub allowed_mode: AllowedMode,
    pub argspec: ParsedArgSpec,
    pub body_mode: ContentMode,
    pub tags: &'static [&'static str],
    pub from_packages: &'static [&'static str],
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct CharacterAttributes {
    pub mathvariant: Option<String>,
    pub tex_class: Option<String>,
    pub stretchy: Option<bool>,
    pub move_sup_sub: Option<bool>,
    pub large_op: Option<bool>,
}

impl From<BuiltinCharacterAttributes> for CharacterAttributes {
    fn from(value: BuiltinCharacterAttributes) -> Self {
        CharacterAttributes {
            mathvariant: value.mathvariant.map(ToString::to_string),
            tex_class: value.tex_class.map(ToString::to_string),
            stretchy: value.stretchy,
            move_sup_sub: value.move_sup_sub,
            large_op: value.large_op,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveCharacterRecord {
    pub name: String,
    pub allowed_mode: AllowedMode,
    pub unicode_value: String,
    pub attributes: CharacterAttributes,
    pub package: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveDelimiterRecord {
    pub name: &'static str,
    pub is_control_sequence: bool,
    pub allowed_mode: AllowedMode,
    pub unicode_value: String,
    pub attributes: CharacterAttributes,
    pub package: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandSpec {
    pub name: String,
    pub kind: CommandKind,
    pub allowed_mode: AllowedMode,
    pub argspec: OwnedArgSpec,
    pub tags: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvironmentSpec {
    pub name: String,
    pub allowed_mode: AllowedMode,
    pub argspec: OwnedArgSpec,
    pub body_mode: ContentMode,
    pub tags: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CharacterSpec {
    pub name: String,
    pub allowed_mode: AllowedMode,
    pub unicode_value: String,
    pub attributes: CharacterAttributes,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DelimiterSpec {
    pub name: String,
    pub is_control_sequence: bool,
    pub allowed_mode: AllowedMode,
    pub unicode_value: String,
    pub attributes: CharacterAttributes,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct PackageSpecs {
    pub characters: Vec<CharacterSpec>,
    pub delimiters: Vec<DelimiterSpec>,
    pub commands: Vec<CommandSpec>,
    pub environments: Vec<EnvironmentSpec>,
}

pub fn load_package_specs_from_str(yaml: &str, context: &str) -> PackageSpecs {
    let parsed: PackageSpecsYaml = serde_yaml::from_str(yaml)
        .unwrap_or_else(|e| panic!("failed to parse package specs ({context}): {e}"));
    parsed.into_specs()
}

impl PackageSpecsYaml {
    fn into_specs(self) -> PackageSpecs {
        PackageSpecs {
            characters: self.characters.into_iter().map(Into::into).collect(),
            delimiters: self.delimiters.into_iter().map(Into::into).collect(),
            commands: self.commands.into_iter().map(Into::into).collect(),
            environments: self.environments.into_iter().map(Into::into).collect(),
        }
    }
}

impl From<CharacterSpecYaml> for CharacterSpec {
    fn from(value: CharacterSpecYaml) -> Self {
        CharacterSpec {
            name: value.name,
            allowed_mode: value.allowed_mode.into(),
            unicode_value: value.unicode_value,
            attributes: value.attributes.into(),
        }
    }
}

impl From<CharacterAttributesYaml> for CharacterAttributes {
    fn from(value: CharacterAttributesYaml) -> Self {
        CharacterAttributes {
            mathvariant: value.mathvariant,
            tex_class: value.tex_class,
            stretchy: value.stretchy,
            move_sup_sub: value.move_sup_sub,
            large_op: value.large_op,
        }
    }
}

impl From<DelimiterSpecYaml> for DelimiterSpec {
    fn from(value: DelimiterSpecYaml) -> Self {
        DelimiterSpec {
            name: value.name,
            is_control_sequence: value.is_control_sequence,
            allowed_mode: value.allowed_mode.into(),
            unicode_value: value.unicode_value,
            attributes: value.attributes.into(),
        }
    }
}

impl From<CommandSpecYaml> for CommandSpec {
    fn from(value: CommandSpecYaml) -> Self {
        let context = format!("command {}", value.name);
        let args =
            parse_arg_specs(value.argspec.as_str(), context.as_str()).unwrap_or_else(|error| {
                panic!("{error}");
            });

        CommandSpec {
            name: value.name,
            kind: value.kind.into(),
            allowed_mode: value.allowed_mode.into(),
            argspec: OwnedArgSpec {
                args,
                source: value.argspec,
            },
            tags: value.tags,
        }
    }
}

impl From<CommandKindYaml> for CommandKind {
    fn from(value: CommandKindYaml) -> Self {
        match value {
            CommandKindYaml::Prefix => CommandKind::Prefix,
            CommandKindYaml::Infix => CommandKind::Infix,
            CommandKindYaml::Declarative => CommandKind::Declarative,
        }
    }
}

impl From<AllowedModeYaml> for AllowedMode {
    fn from(value: AllowedModeYaml) -> Self {
        match value {
            AllowedModeYaml::Math => AllowedMode::Math,
            AllowedModeYaml::Text => AllowedMode::Text,
            AllowedModeYaml::Both => AllowedMode::Both,
        }
    }
}

impl From<EnvironmentSpecYaml> for EnvironmentSpec {
    fn from(value: EnvironmentSpecYaml) -> Self {
        let context = format!("environment {}", value.name);
        let args =
            parse_arg_specs(value.argspec.as_str(), context.as_str()).unwrap_or_else(|error| {
                panic!("{error}");
            });

        EnvironmentSpec {
            name: value.name,
            allowed_mode: value.allowed_mode.into(),
            argspec: OwnedArgSpec {
                args,
                source: value.argspec,
            },
            body_mode: value.body_mode.into(),
            tags: value.tags,
        }
    }
}

impl From<ContentModeYaml> for ContentMode {
    fn from(value: ContentModeYaml) -> Self {
        match value {
            ContentModeYaml::Math => ContentMode::Math,
            ContentModeYaml::Text => ContentMode::Text,
        }
    }
}