Skip to main content

hk_parser/
lib.rs

1// src/lib.rs
2//! Hacker Lang Configuration Parser
3//!
4//! This crate provides a robust parser and serializer for .hk files used in Hacker Lang.
5//! It supports nested structures, comments, and error handling.
6use indexmap::IndexMap;
7use lazy_static::lazy_static;
8use nom::{
9    branch::alt,
10    bytes::complete::{tag, take_until, take_while, take_while1},
11    character::complete::{multispace0, multispace1},
12    combinator::{eof, map, opt, peek},
13    error::{context, VerboseError, VerboseErrorKind},
14    multi::{many0, many1, separated_list0},
15    sequence::{delimited, preceded, terminated, tuple},
16    IResult,
17};
18use nom_locate::LocatedSpan;
19use regex::Regex;
20use std::env;
21use std::fs::File;
22use std::io::{self, BufRead, BufReader, Write};
23use std::path::Path;
24use std::str::FromStr;
25use thiserror::Error;
26type Span<'a> = LocatedSpan<&'a str>;
27type ParseResult<'a, T> = IResult<Span<'a>, T, VerboseError<Span<'a>>>;
28/// Represents the structure of a .hk file.
29/// Sections are top-level keys in the outer IndexMap to preserve order.
30pub type HkConfig = IndexMap<String, HkValue>;
31lazy_static! {
32    static ref INTERPOL_RE: Regex = Regex::new(r"\$\{([^}]+)\}").unwrap();
33}
34/// Enum for values in the .hk config: supports strings, numbers, booleans, arrays, and maps.
35#[derive(Debug, Clone, PartialEq)]
36pub enum HkValue {
37    String(String),
38    Number(f64),
39    Bool(bool),
40    Array(Vec<HkValue>),
41    Map(IndexMap<String, HkValue>),
42}
43impl HkValue {
44    pub fn as_string(&self) -> Result<String, HkError> {
45        if let Self::String(s) = self {
46            Ok(s.clone())
47        } else {
48            Err(HkError::TypeMismatch {
49                expected: "string".to_string(),
50                found: format!("{:?}", self),
51            })
52        }
53    }
54    pub fn as_number(&self) -> Result<f64, HkError> {
55        if let Self::Number(n) = self {
56            Ok(*n)
57        } else {
58            Err(HkError::TypeMismatch {
59                expected: "number".to_string(),
60                found: format!("{:?}", self),
61            })
62        }
63    }
64    pub fn as_bool(&self) -> Result<bool, HkError> {
65        if let Self::Bool(b) = self {
66            Ok(*b)
67        } else {
68            Err(HkError::TypeMismatch {
69                expected: "bool".to_string(),
70                found: format!("{:?}", self),
71            })
72        }
73    }
74    pub fn as_array(&self) -> Result<&Vec<HkValue>, HkError> {
75        if let Self::Array(a) = self {
76            Ok(a)
77        } else {
78            Err(HkError::TypeMismatch {
79                expected: "array".to_string(),
80                found: format!("{:?}", self),
81            })
82        }
83    }
84    pub fn as_map(&self) -> Result<&IndexMap<String, HkValue>, HkError> {
85        if let Self::Map(m) = self {
86            Ok(m)
87        } else {
88            Err(HkError::TypeMismatch {
89                expected: "map".to_string(),
90                found: format!("{:?}", self),
91            })
92        }
93    }
94}
95/// Custom error type for parsing .hk files.
96#[derive(Error, Debug)]
97pub enum HkError {
98    #[error("IO error: {0}")]
99    Io(#[from] io::Error),
100    #[error("Parse error at line {line}, column {column}: {message}")]
101    Parse {
102        line: u32,
103        column: usize,
104        message: String,
105    },
106    #[error("Type mismatch: expected {expected}, found {found}")]
107    TypeMismatch { expected: String, found: String },
108    #[error("Missing field: {0}")]
109    MissingField(String),
110    #[error("Invalid reference: {0}")]
111    InvalidReference(String),
112}
113/// Parses a .hk file from a string input.
114pub fn parse_hk(input: &str) -> Result<HkConfig, HkError> {
115    let input_span = LocatedSpan::new(input);
116    let mut remaining = input_span;
117    let mut config = IndexMap::new();
118
119    while !remaining.fragment().is_empty() {
120        // ZMIANA: Czyścimy wszystko (spacje, komentarze) ZANIM spróbujemy parsować sekcję
121        let (rest, _) = many0(alt((
122            multispace1,
123            map(comment, |_| Span::new("")) 
124        )))(remaining).map_err(|e| map_nom_error(input, remaining, e))?;
125        
126        remaining = rest;
127        if remaining.fragment().is_empty() { break; }
128
129        let (rest, (name, values)) = section(remaining).map_err(|e| map_nom_error(input, remaining, e))?;
130        config.insert(name, HkValue::Map(values));
131        remaining = rest;
132    }
133    Ok(config)
134}
135/// Helper to map nom error to HkError.
136fn map_nom_error(input: &str, span: Span, err: nom::Err<VerboseError<Span>>) -> HkError {
137    let verbose_err = match err {
138        nom::Err::Error(e) | nom::Err::Failure(e) => e,
139        nom::Err::Incomplete(_) => VerboseError { errors: vec![] },
140    };
141   
142    // Use the first error location for line/column
143    let (line, column) = if let Some((s, _)) = verbose_err.errors.first() {
144        (s.location_line(), s.get_column())
145    } else {
146        (span.location_line(), span.get_column())
147    };
148    // Convert VerboseError<LocatedSpan<&str>> to VerboseError<&str>
149    let errors_str: Vec<(&str, VerboseErrorKind)> = verbose_err
150        .errors
151        .iter()
152        .map(|(s, k)| (*s.fragment(), k.clone()))
153        .collect();
154    let verbose_err_str = VerboseError { errors: errors_str };
155    let mut message = nom::error::convert_error(input, verbose_err_str);
156    // Dodatkowe ulepszenie: Dodaj kontekstowe wskazówki dla typowych błędów
157    if message.contains("tag \"=>\"") {
158        message.push_str("\nHint: Upewnij się, że po kluczu znajduje się '=>' przed wartością.");
159    } else if message.contains("tag \"[\"") {
160        message.push_str("\nHint: Sprawdź, czy sekcje zaczynają się od '[' i kończą ']'.");
161    } else if message.contains("take_while1") {
162        message.push_str("\nHint: Klucze mogą zawierać tylko litery, cyfry i '_'.");
163    }
164    HkError::Parse {
165        line,
166        column,
167        message,
168    }
169}
170/// Loads and parses a .hk file from the given path.
171pub fn load_hk_file<P: AsRef<Path>>(path: P) -> Result<HkConfig, HkError> {
172    let file = File::open(path)?;
173    let reader = BufReader::new(file);
174    let mut contents = String::new();
175    for line in reader.lines() {
176        let line = line?;
177        contents.push_str(&line);
178        contents.push('\n');
179    }
180    parse_hk(&contents)
181}
182/// Resolves interpolations in the config, including env vars and references.
183pub fn resolve_interpolations(config: &mut HkConfig) -> Result<(), HkError> {
184    // Clone the config to use as a read-only context while we mutate the original
185    let context = config.clone();
186   
187    for (_, value) in config.iter_mut() {
188        if let HkValue::Map(map) = value {
189            resolve_map(map, &context)?;
190        }
191    }
192    Ok(())
193}
194fn resolve_map(map: &mut IndexMap<String, HkValue>, top: &HkConfig) -> Result<(), HkError> {
195    for (_, v) in map.iter_mut() {
196        resolve_value(v, top)?;
197    }
198    Ok(())
199}
200fn resolve_value(v: &mut HkValue, top: &HkConfig) -> Result<(), HkError> {
201    match v {
202        HkValue::String(s) => {
203            let mut new_s = String::new();
204            let mut last = 0;
205            for cap in INTERPOL_RE.captures_iter(s) {
206                let m = cap.get(0).unwrap();
207                new_s.push_str(&s[last..m.start()]);
208                let var = &cap[1];
209                let repl = if var.starts_with("env:") {
210                    env::var(&var[4..]).unwrap_or_default()
211                } else {
212                    resolve_path(var, top).ok_or(HkError::InvalidReference(var.to_string()))?
213                };
214                new_s.push_str(&repl);
215                last = m.end();
216            }
217            new_s.push_str(&s[last..]);
218            *s = new_s;
219        }
220        HkValue::Array(a) => {
221            for item in a.iter_mut() {
222                resolve_value(item, top)?;
223            }
224        }
225        HkValue::Map(m) => resolve_map(m, top)?,
226        _ => {}
227    }
228    Ok(())
229}
230fn resolve_path(path: &str, config: &HkConfig) -> Option<String> {
231    let parts: Vec<&str> = path.split('.').collect();
232    let mut current: Option<&HkValue> = config.get(parts[0]);
233    for &p in &parts[1..] {
234        current = current.and_then(|v| v.as_map().ok()).and_then(|m| m.get(p));
235    }
236    current.and_then(|v| v.as_string().ok())
237}
238/// Serializes a HkConfig back to a .hk string, preserving key order.
239pub fn serialize_hk(config: &HkConfig) -> String {
240    let mut output = String::new();
241    for (section, value) in config.iter() {
242        output.push_str(&format!("[{}]\n", section));
243        if let HkValue::Map(map) = value {
244            serialize_map(map, 0, &mut output);
245        }
246        output.push('\n');
247    }
248    output.trim_end().to_string()
249}
250fn serialize_map(map: &IndexMap<String, HkValue>, indent: usize, output: &mut String) {
251    let spaces = " ".repeat(indent);
252    for (key, value) in map.iter() {
253        match value {
254            HkValue::Map(submap) => {
255                output.push_str(&format!("{}-> {}\n", spaces, key));
256                serialize_map(submap, indent + 1, output);
257            }
258            _ => {
259                output.push_str(&format!("{}-> {} => {}\n", spaces, key, serialize_value(value)));
260            }
261        }
262    }
263}
264fn serialize_value(value: &HkValue) -> String {
265    match value {
266        HkValue::String(s) => {
267            if s.contains(',') || s.contains(' ') || s.contains(']') || s.contains('"') {
268                format!("\"{}\"", s.replace("\"", "\\\""))
269            } else {
270                s.clone()
271            }
272        }
273        HkValue::Number(n) => n.to_string(),
274        HkValue::Bool(b) => if *b { "true".to_string() } else { "false".to_string() },
275        HkValue::Array(a) => format!(
276            "[{}]",
277            a.iter()
278                .map(serialize_value)
279                .collect::<Vec<_>>()
280                .join(", ")
281        ),
282        HkValue::Map(_) => "<map>".to_string(), // Maps are serialized as nested, not inline
283    }
284}
285/// Writes a HkConfig to a file.
286pub fn write_hk_file<P: AsRef<Path>>(path: P, config: &HkConfig) -> io::Result<()> {
287    let mut file = File::create(path)?;
288    file.write_all(serialize_hk(config).as_bytes())
289}
290// Parser combinators
291fn comment(input: Span) -> ParseResult<Span> {
292    context(
293        "comment",
294        delimited(tag("!"), take_while(|c| c != '\r' && c != '\n'), opt(tag("\n"))),
295    )(input)
296}
297fn section(input: Span) -> ParseResult<(String, IndexMap<String, HkValue>)> {
298    context(
299        "section",
300        map(
301            tuple((
302                delimited(tag("["), take_until("]"), tag("]")),
303                multispace0,
304                terminated(
305                    many0(alt((
306                        map(comment, |_| None),
307                        map(key_value, Some),
308                        map(nested_key_value, Some),
309                    ))),
310                    // TUTAJ ZMIANA: multispace0 przed peekiem czyści śmieci po ostatnim kluczu
311                    tuple((multispace0, peek(alt((tag("["), map(eof, |_| Span::new(""))))))),
312                ),
313            )),
314            |(name, _, opt_pairs)| {
315                let mut map = IndexMap::new();
316                for pair_opt in opt_pairs {
317                    if let Some((key, value)) = pair_opt {
318                        insert_nested(&mut map, key.split('.').collect::<Vec<_>>(), value);
319                    }
320                }
321                (name.fragment().trim().to_string(), map)
322            },
323        ),
324    )(input)
325}
326/// Inserts a value into a nested map using dot-separated keys.
327fn insert_nested(map: &mut IndexMap<String, HkValue>, keys: Vec<&str>, value: HkValue) {
328    let mut current = map;
329    for key in &keys[0..keys.len() - 1] {
330        let entry = current
331            .entry(key.to_string())
332            .or_insert(HkValue::Map(IndexMap::new()));
333        if let HkValue::Map(submap) = entry {
334            current = submap;
335        } else {
336            panic!("Invalid nesting");
337        }
338    }
339    if let Some(last_key) = keys.last() {
340        current.insert(last_key.to_string(), value);
341    }
342}
343fn key_value(input: Span) -> ParseResult<(String, HkValue)> {
344    context(
345        "key_value",
346        map(
347            tuple((
348                preceded(
349                    tuple((multispace0, tag("->"), multispace1)),
350                    take_while1(|c: char| c.is_alphanumeric() || c == '_'),
351                ),
352                multispace0,
353                tag("=>"),
354                line_value,
355            )),
356            |(key, _, _, value)| (key.fragment().trim().to_string(), value),
357        ),
358    )(input)
359}
360fn nested_key_value(input: Span) -> ParseResult<(String, HkValue)> {
361    context(
362        "nested_key_value",
363        map(
364            tuple((
365                preceded(
366                    tuple((multispace0, tag("->"), multispace1)),
367                    take_while1(|c: char| c.is_alphanumeric() || c == '_'),
368                ),
369                many1(sub_key_value),
370            )),
371            |(key, sub_pairs)| {
372                let mut sub_map = IndexMap::new();
373                for (sub_key, sub_value) in sub_pairs {
374                    sub_map.insert(sub_key, sub_value);
375                }
376                (key.fragment().trim().to_string(), HkValue::Map(sub_map))
377            },
378        ),
379    )(input)
380}
381fn sub_key_value(input: Span) -> ParseResult<(String, HkValue)> {
382    context(
383        "sub_key_value",
384        map(
385            tuple((
386                preceded(
387                    tuple((multispace1, tag("-->"), multispace1)),
388                    take_while1(|c: char| c.is_alphanumeric() || c == '_'),
389                ),
390                multispace0,
391                tag("=>"),
392                line_value,
393            )),
394            |(sub_key, _, _, sub_value)| (sub_key.fragment().trim().to_string(), sub_value),
395        ),
396    )(input)
397}
398fn line_value(input: Span) -> ParseResult<HkValue> {
399    preceded(
400        multispace0,
401        alt((
402            map(array, HkValue::Array),
403            map(
404                terminated(take_while(|c| c != '\r' && c != '\n'), opt(tag("\n"))),
405                |s: Span| parse_simple(s.fragment()),
406            ),
407        )),
408    )(input)
409}
410fn parse_simple(s: &str) -> HkValue {
411    let s = s.trim();
412    if s.eq_ignore_ascii_case("true") {
413        HkValue::Bool(true)
414    } else if s.eq_ignore_ascii_case("false") {
415        HkValue::Bool(false)
416    } else if let Ok(n) = f64::from_str(s) {
417        HkValue::Number(n)
418    } else {
419        HkValue::String(s.to_string())
420    }
421}
422fn array(input: Span) -> ParseResult<Vec<HkValue>> {
423    delimited(
424        tag("["),
425        separated_list0(tuple((multispace0, tag(","), multispace0)), item_value),
426        tag("]"),
427    )(input)
428    .map(|(i, v)| (i, v))
429}
430fn item_value(input: Span) -> ParseResult<HkValue> {
431    alt((
432        map(array, HkValue::Array),
433        map(double_quoted, |s| HkValue::String(s.fragment().to_string())),
434        map(
435            take_while1(|c: char| !c.is_whitespace() && c != ',' && c != ']'),
436            |s: Span| parse_simple(s.fragment()),
437        ),
438    ))(input)
439}
440fn double_quoted(input: Span) -> ParseResult<Span> {
441    delimited(tag("\""), take_while(|c| c != '"'), tag("\""))(input)
442}
443pub trait FromHkValue: Sized {
444    fn from_hk_value(value: &HkValue) -> Result<Self, HkError>;
445}
446impl FromHkValue for String {
447    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
448        value.as_string()
449    }
450}
451impl FromHkValue for f64 {
452    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
453        value.as_number()
454    }
455}
456impl FromHkValue for bool {
457    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
458        value.as_bool()
459    }
460}
461impl<T: FromHkValue> FromHkValue for Vec<T> {
462    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
463        value
464            .as_array()?
465            .iter()
466            .map(|v| T::from_hk_value(v))
467            .collect()
468    }
469}
470impl<T: FromHkValue> FromHkValue for Option<T> {
471    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
472        Ok(Some(T::from_hk_value(value)?))
473    }
474}
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use pretty_assertions::assert_eq;
479    #[test]
480    fn test_parse_hk_with_comments_and_types() {
481        let input = r#"
482        ! Globalne informacje o projekcie
483        [metadata]
484        -> name => Hacker Lang
485        -> version => 1.5
486        -> authors => HackerOS Team <hackeros068@gmail.com>
487        -> license => MIT
488        -> is_active => true
489        -> pi => 3.14
490        -> list => [1, 2.5, true, "four"]
491        [description]
492        -> summary => Programing language for HackerOS.
493        -> long => Język programowania Hacker Lang z plikami konfiguracyjnymi .hk lub .hacker lub skryptami itd. .hl.
494        [specs]
495        -> rust => >= 1.92.0
496        -> dependencies
497        --> odin => >= 2026-01
498        --> c => C23
499        --> crystal => 1.19.0
500        --> python => 3.13
501        "#;
502        let result = parse_hk(input).unwrap();
503        assert_eq!(result.len(), 3);
504        if let Some(HkValue::Map(metadata)) = result.get("metadata") {
505            assert_eq!(metadata.len(), 7);
506            assert_eq!(
507                metadata.get("name"),
508                Some(&HkValue::String("Hacker Lang".to_string()))
509            );
510            assert_eq!(
511                metadata.get("version"),
512                Some(&HkValue::Number(1.5))
513            );
514            assert_eq!(
515                metadata.get("authors"),
516                Some(&HkValue::String("HackerOS Team <hackeros068@gmail.com>".to_string()))
517            );
518            assert_eq!(
519                metadata.get("license"),
520                Some(&HkValue::String("MIT".to_string()))
521            );
522            assert_eq!(
523                metadata.get("is_active"),
524                Some(&HkValue::Bool(true))
525            );
526            assert_eq!(
527                metadata.get("pi"),
528                Some(&HkValue::Number(3.14))
529            );
530            assert_eq!(
531                metadata.get("list"),
532                Some(&HkValue::Array(vec![
533                    HkValue::Number(1.0),
534                    HkValue::Number(2.5),
535                    HkValue::Bool(true),
536                    HkValue::String("four".to_string()),
537                ]))
538            );
539        }
540    }
541    #[test]
542    fn test_serialize_hk() {
543        let mut config = IndexMap::new();
544        let mut metadata = IndexMap::new();
545        metadata.insert("name".to_string(), HkValue::String("Hacker Lang".to_string()));
546        metadata.insert("version".to_string(), HkValue::Number(1.5));
547        config.insert("metadata".to_string(), HkValue::Map(metadata));
548        let serialized = serialize_hk(&config);
549        assert!(serialized.contains("[metadata]"));
550        // Serializer adds quotes to strings containing spaces
551        assert!(serialized.contains("-> name => \"Hacker Lang\""));
552        assert!(serialized.contains("-> version => 1.5"));
553    }
554    #[test]
555    fn test_error_handling() {
556        let invalid_input = r#"
557        [metadata]
558        -> name = Hacker Lang # Missing =>
559        "#;
560        let err = parse_hk(invalid_input).unwrap_err();
561        if let HkError::Parse { line, message, .. } = err {
562            assert_eq!(line, 3);
563            // Relaxed check as error message might vary depending on nom version and verbosity
564            assert!(!message.is_empty());
565        } else {
566            panic!("Unexpected error type");
567        }
568    }
569    #[test]
570    fn test_interpolation() {
571        let mut config = IndexMap::new();
572        let mut metadata = IndexMap::new();
573        metadata.insert("name".to_string(), HkValue::String("Hacker Lang".to_string()));
574        let mut path = IndexMap::new();
575        path.insert("bin".to_string(), HkValue::String("${metadata.name}/bin".to_string()));
576        config.insert("metadata".to_string(), HkValue::Map(metadata));
577        config.insert("path".to_string(), HkValue::Map(path));
578        resolve_interpolations(&mut config).unwrap();
579        if let Some(HkValue::Map(p)) = config.get("path") {
580            if let Some(HkValue::String(s)) = p.get("bin") {
581                assert_eq!(s, "Hacker Lang/bin");
582            }
583        }
584    }
585}