Skip to main content

texform_knowledge/
specs.rs

1//! Shared spec types.
2//!
3//! This crate hosts:
4//! - `PackageSpecs`: parsed YAML package specs (owned, merge-ready)
5//! - builtin records generated at compile time
6//! - active records assembled by the knowledge base at runtime
7
8#[path = "specs_yaml.rs"]
9mod specs_yaml;
10
11use specs_yaml::{
12    AllowedModeYaml, CharacterAttributesYaml, CharacterSpecYaml, CommandKindYaml, CommandSpecYaml,
13    ContentModeYaml, DelimiterSpecYaml, EnvironmentSpecYaml, PackageSpecsYaml,
14};
15
16pub use texform_argspec::ContentMode;
17pub use texform_argspec::{
18    ArgForm, ArgSpec, ArgSpecParseError, DelimiterToken, OwnedArgSpec, ParsedArgSpec, ValueKind,
19    parse_arg_specs,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum CommandKind {
24    Prefix,
25    Infix,
26    Declarative,
27}
28
29impl CommandKind {
30    pub const fn label(&self) -> &'static str {
31        match self {
32            CommandKind::Prefix => "prefix",
33            CommandKind::Infix => "infix",
34            CommandKind::Declarative => "declarative",
35        }
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum AllowedMode {
41    Math,
42    Text,
43    Both,
44}
45
46impl AllowedMode {
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            AllowedMode::Math => "math",
50            AllowedMode::Text => "text",
51            AllowedMode::Both => "both",
52        }
53    }
54
55    pub const fn union(self, other: Self) -> Self {
56        match (self, other) {
57            (AllowedMode::Both, _) | (_, AllowedMode::Both) => AllowedMode::Both,
58            (AllowedMode::Math, AllowedMode::Math) => AllowedMode::Math,
59            (AllowedMode::Text, AllowedMode::Text) => AllowedMode::Text,
60            (AllowedMode::Math, AllowedMode::Text) | (AllowedMode::Text, AllowedMode::Math) => {
61                AllowedMode::Both
62            }
63        }
64    }
65
66    pub const fn allows(self, mode: ContentMode) -> bool {
67        match self {
68            AllowedMode::Both => true,
69            AllowedMode::Math => matches!(mode, ContentMode::Math),
70            AllowedMode::Text => matches!(mode, ContentMode::Text),
71        }
72    }
73}
74
75impl std::fmt::Display for AllowedMode {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.write_str((*self).as_str())
78    }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct BuiltinCommandRecord {
83    pub name: &'static str,
84    pub kind: CommandKind,
85    pub allowed_mode: AllowedMode,
86    pub argspec: ParsedArgSpec,
87    pub tags: &'static [&'static str],
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct BuiltinEnvironmentRecord {
92    pub name: &'static str,
93    pub allowed_mode: AllowedMode,
94    pub argspec: ParsedArgSpec,
95    pub body_mode: ContentMode,
96    pub tags: &'static [&'static str],
97}
98
99#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
100pub struct BuiltinCharacterAttributes {
101    pub mathvariant: Option<&'static str>,
102    pub tex_class: Option<&'static str>,
103    pub stretchy: Option<bool>,
104    pub move_sup_sub: Option<bool>,
105    pub large_op: Option<bool>,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct BuiltinCharacterRecord {
110    pub name: &'static str,
111    pub allowed_mode: AllowedMode,
112    pub unicode_value: &'static str,
113    pub attributes: BuiltinCharacterAttributes,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct BuiltinDelimiterRecord {
118    pub name: &'static str,
119    pub is_control_sequence: bool,
120    pub allowed_mode: AllowedMode,
121    pub unicode_value: &'static str,
122    pub attributes: BuiltinCharacterAttributes,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct ActiveCommandRecord {
127    pub name: &'static str,
128    pub kind: CommandKind,
129    pub allowed_mode: AllowedMode,
130    pub argspec: ParsedArgSpec,
131    pub tags: &'static [&'static str],
132    pub from_packages: &'static [&'static str],
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct ActiveEnvironmentRecord {
137    pub name: &'static str,
138    pub allowed_mode: AllowedMode,
139    pub argspec: ParsedArgSpec,
140    pub body_mode: ContentMode,
141    pub tags: &'static [&'static str],
142    pub from_packages: &'static [&'static str],
143}
144
145#[derive(Debug, Default, Clone, PartialEq, Eq)]
146pub struct CharacterAttributes {
147    pub mathvariant: Option<String>,
148    pub tex_class: Option<String>,
149    pub stretchy: Option<bool>,
150    pub move_sup_sub: Option<bool>,
151    pub large_op: Option<bool>,
152}
153
154impl From<BuiltinCharacterAttributes> for CharacterAttributes {
155    fn from(value: BuiltinCharacterAttributes) -> Self {
156        CharacterAttributes {
157            mathvariant: value.mathvariant.map(ToString::to_string),
158            tex_class: value.tex_class.map(ToString::to_string),
159            stretchy: value.stretchy,
160            move_sup_sub: value.move_sup_sub,
161            large_op: value.large_op,
162        }
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct ActiveCharacterRecord {
168    pub name: String,
169    pub allowed_mode: AllowedMode,
170    pub unicode_value: String,
171    pub attributes: CharacterAttributes,
172    pub package: String,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct ActiveDelimiterRecord {
177    pub name: &'static str,
178    pub is_control_sequence: bool,
179    pub allowed_mode: AllowedMode,
180    pub unicode_value: String,
181    pub attributes: CharacterAttributes,
182    pub package: String,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct CommandSpec {
187    pub name: String,
188    pub kind: CommandKind,
189    pub allowed_mode: AllowedMode,
190    pub argspec: OwnedArgSpec,
191    pub tags: Vec<String>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct EnvironmentSpec {
196    pub name: String,
197    pub allowed_mode: AllowedMode,
198    pub argspec: OwnedArgSpec,
199    pub body_mode: ContentMode,
200    pub tags: Vec<String>,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct CharacterSpec {
205    pub name: String,
206    pub allowed_mode: AllowedMode,
207    pub unicode_value: String,
208    pub attributes: CharacterAttributes,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct DelimiterSpec {
213    pub name: String,
214    pub is_control_sequence: bool,
215    pub allowed_mode: AllowedMode,
216    pub unicode_value: String,
217    pub attributes: CharacterAttributes,
218}
219
220#[derive(Debug, Default, Clone, PartialEq, Eq)]
221pub struct PackageSpecs {
222    pub characters: Vec<CharacterSpec>,
223    pub delimiters: Vec<DelimiterSpec>,
224    pub commands: Vec<CommandSpec>,
225    pub environments: Vec<EnvironmentSpec>,
226}
227
228pub fn load_package_specs_from_str(yaml: &str, context: &str) -> PackageSpecs {
229    let parsed: PackageSpecsYaml = serde_yaml::from_str(yaml)
230        .unwrap_or_else(|e| panic!("failed to parse package specs ({context}): {e}"));
231    parsed.into_specs()
232}
233
234impl PackageSpecsYaml {
235    fn into_specs(self) -> PackageSpecs {
236        PackageSpecs {
237            characters: self.characters.into_iter().map(Into::into).collect(),
238            delimiters: self.delimiters.into_iter().map(Into::into).collect(),
239            commands: self.commands.into_iter().map(Into::into).collect(),
240            environments: self.environments.into_iter().map(Into::into).collect(),
241        }
242    }
243}
244
245impl From<CharacterSpecYaml> for CharacterSpec {
246    fn from(value: CharacterSpecYaml) -> Self {
247        CharacterSpec {
248            name: value.name,
249            allowed_mode: value.allowed_mode.into(),
250            unicode_value: value.unicode_value,
251            attributes: value.attributes.into(),
252        }
253    }
254}
255
256impl From<CharacterAttributesYaml> for CharacterAttributes {
257    fn from(value: CharacterAttributesYaml) -> Self {
258        CharacterAttributes {
259            mathvariant: value.mathvariant,
260            tex_class: value.tex_class,
261            stretchy: value.stretchy,
262            move_sup_sub: value.move_sup_sub,
263            large_op: value.large_op,
264        }
265    }
266}
267
268impl From<DelimiterSpecYaml> for DelimiterSpec {
269    fn from(value: DelimiterSpecYaml) -> Self {
270        DelimiterSpec {
271            name: value.name,
272            is_control_sequence: value.is_control_sequence,
273            allowed_mode: value.allowed_mode.into(),
274            unicode_value: value.unicode_value,
275            attributes: value.attributes.into(),
276        }
277    }
278}
279
280impl From<CommandSpecYaml> for CommandSpec {
281    fn from(value: CommandSpecYaml) -> Self {
282        let context = format!("command {}", value.name);
283        let args =
284            parse_arg_specs(value.argspec.as_str(), context.as_str()).unwrap_or_else(|error| {
285                panic!("{error}");
286            });
287
288        CommandSpec {
289            name: value.name,
290            kind: value.kind.into(),
291            allowed_mode: value.allowed_mode.into(),
292            argspec: OwnedArgSpec {
293                args,
294                source: value.argspec,
295            },
296            tags: value.tags,
297        }
298    }
299}
300
301impl From<CommandKindYaml> for CommandKind {
302    fn from(value: CommandKindYaml) -> Self {
303        match value {
304            CommandKindYaml::Prefix => CommandKind::Prefix,
305            CommandKindYaml::Infix => CommandKind::Infix,
306            CommandKindYaml::Declarative => CommandKind::Declarative,
307        }
308    }
309}
310
311impl From<AllowedModeYaml> for AllowedMode {
312    fn from(value: AllowedModeYaml) -> Self {
313        match value {
314            AllowedModeYaml::Math => AllowedMode::Math,
315            AllowedModeYaml::Text => AllowedMode::Text,
316            AllowedModeYaml::Both => AllowedMode::Both,
317        }
318    }
319}
320
321impl From<EnvironmentSpecYaml> for EnvironmentSpec {
322    fn from(value: EnvironmentSpecYaml) -> Self {
323        let context = format!("environment {}", value.name);
324        let args =
325            parse_arg_specs(value.argspec.as_str(), context.as_str()).unwrap_or_else(|error| {
326                panic!("{error}");
327            });
328
329        EnvironmentSpec {
330            name: value.name,
331            allowed_mode: value.allowed_mode.into(),
332            argspec: OwnedArgSpec {
333                args,
334                source: value.argspec,
335            },
336            body_mode: value.body_mode.into(),
337            tags: value.tags,
338        }
339    }
340}
341
342impl From<ContentModeYaml> for ContentMode {
343    fn from(value: ContentModeYaml) -> Self {
344        match value {
345            ContentModeYaml::Math => ContentMode::Math,
346            ContentModeYaml::Text => ContentMode::Text,
347        }
348    }
349}