vmf_forge/
parser.rs

1//! This module provides the VMF parser implementation using the `pest` parsing library.
2
3use indexmap::IndexMap;
4use pest::iterators::Pair;
5use pest::Parser;
6use pest_derive::Parser;
7
8use crate::errors::{VmfError, VmfResult};
9
10use crate::vmf::regions::{Cordon, Cordons};
11use crate::{Cameras, Entity, VersionInfo, ViewSettings, VisGroups, VmfBlock, VmfFile, World};
12
13/// The VMF parser.
14#[derive(Parser)]
15#[grammar = "vmf.pest"]
16struct VmfParser;
17
18/// Parses a VMF string into a `VmfFile` struct.
19///
20/// # Arguments
21///
22/// * `input` - The VMF string to parse.
23///
24/// # Returns
25///
26/// A `VmfResult` containing the parsed `VmfFile` or a `VmfError` if parsing fails.
27pub fn parse_vmf(input: &str) -> VmfResult<VmfFile> {
28    let parsed = VmfParser::parse(Rule::file, input)?.next().unwrap();
29    let mut vmf_file = VmfFile::default();
30
31    for pair in parsed.into_inner() {
32        if pair.as_rule() == Rule::block {
33            let block: VmfBlock = parse_block(pair)?;
34
35            match block.name.to_lowercase().as_str() {
36                // -- metadatas
37                "versioninfo" => vmf_file.versioninfo = VersionInfo::try_from(block)?,
38                "visgroups" => vmf_file.visgroups = VisGroups::try_from(block)?,
39                "viewsettings" => vmf_file.viewsettings = ViewSettings::try_from(block)?,
40
41                // world
42                "world" => vmf_file.world = World::try_from(block)?,
43
44                // -- entities
45                "entity" => vmf_file.entities.push(Entity::try_from(block)?),
46                "hidden" => {
47                    if let Some(hidden_block) = block.blocks.first() {
48                        let mut ent = Entity::try_from(hidden_block.to_owned())?;
49                        ent.is_hidden = true;
50                        vmf_file.hiddens.push(ent)
51                    }
52                }
53
54                // -- regions
55                "cameras" => vmf_file.cameras = Cameras::try_from(block)?,
56                "cordons" => vmf_file.cordons = Cordons::try_from(block)?,
57                // for old version of VMF
58                "cordon" => vmf_file.cordons.push(Cordon::try_from(block)?),
59                // ....
60                _ => {
61                    #[cfg(feature = "debug_assert_info")]
62                    debug_assert!(false, "Unexpected block name: {}", block.name);
63                }
64            }
65        }
66    }
67
68    Ok(vmf_file)
69}
70
71/// Parses a `Pair` representing a VMF block into a `VmfBlock` struct.
72///
73/// # Arguments
74///
75/// * `pair` - The `Pair` representing the VMF block.
76///
77/// # Returns
78///
79/// A `VmfResult` containing the parsed `VmfBlock` or a `VmfError` if parsing fails.
80fn parse_block(pair: Pair<Rule>) -> VmfResult<VmfBlock> {
81    let mut inner = pair.into_inner();
82    let block_name_pair = inner
83        .next()
84        .ok_or(VmfError::InvalidFormat("block name not found".to_string()))?;
85
86    let name = block_name_pair.as_str().to_string();
87
88    let mut key_values = IndexMap::new();
89    let mut blocks = Vec::new();
90
91    for item in inner {
92        match item.as_rule() {
93            Rule::key_value => {
94                let mut kv_inner = item.into_inner();
95                let key = strip_quotes(
96                    kv_inner
97                        .next()
98                        .ok_or(VmfError::InvalidFormat("key not found".to_string()))?
99                        .as_str(),
100                );
101                let value = strip_quotes(
102                    kv_inner
103                        .next()
104                        .ok_or(VmfError::InvalidFormat("value not found".to_string()))?
105                        .as_str(),
106                );
107
108                key_values
109                    .entry(key)
110                    .and_modify(|existing_value: &mut String| {
111                        existing_value.push('\r');
112                        existing_value.push_str(&value);
113                    })
114                    .or_insert(value);
115            }
116            Rule::block => {
117                blocks.push(parse_block(item)?);
118            }
119            _ => {}
120        }
121    }
122
123    Ok(VmfBlock {
124        name,
125        key_values,
126        blocks,
127    })
128}
129
130/// Removes the leading and trailing quotes from a string.
131///
132/// # Arguments
133///
134/// * `s` - The string to strip quotes from.
135///
136/// # Returns
137///
138/// The string with quotes removed.
139fn strip_quotes(s: &str) -> String {
140    s.trim_matches('"').to_string()
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    #[test]
147    fn parse_block_valid_block() {
148        let input = "entity { \"classname\" \"logic_relay\" }";
149        let mut parsed = VmfParser::parse(Rule::block, input).unwrap();
150        let block = parse_block(parsed.next().unwrap()).unwrap();
151
152        assert_eq!(block.name, "entity");
153        assert_eq!(
154            block.key_values.get("classname"),
155            Some(&"logic_relay".to_string())
156        );
157        assert!(block.blocks.is_empty());
158    }
159
160    #[test]
161    fn parse_block_nested_blocks() {
162        let input = "entity { \"classname\" \"logic_relay\" solid { \"id\" \"1\" } }";
163        let mut parsed = VmfParser::parse(Rule::block, input).unwrap();
164        let block = parse_block(parsed.next().unwrap()).unwrap();
165
166        assert_eq!(block.name, "entity");
167        assert_eq!(
168            block.key_values.get("classname"),
169            Some(&"logic_relay".to_string())
170        );
171        assert_eq!(block.blocks.len(), 1);
172        assert_eq!(block.blocks[0].name, "solid");
173        assert_eq!(block.blocks[0].key_values.get("id"), Some(&"1".to_string()));
174    }
175
176    #[test]
177    fn parse_block_empty_block() {
178        let input = "entity { }";
179        let mut parsed = VmfParser::parse(Rule::block, input).unwrap();
180        let block = parse_block(parsed.next().unwrap()).unwrap();
181
182        assert_eq!(block.name, "entity");
183        assert!(block.key_values.is_empty());
184        assert!(block.blocks.is_empty());
185    }
186}