dynamic_html/
parser.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use regex::Regex;

use crate::DynamicHtml;

#[derive(Debug, Clone)]
pub enum HtmlPart {
    Literal(String),
    Eval(String),
    Unescaped(String),
    Block(String),
    Import(String),
}

#[derive(Debug, Clone)]
pub struct HtmlImportPart {
    pub content: String,
    pub filename: String,
    pub default_name: Option<String>,
    pub imports: Option<String>,
}

#[derive(Debug, Clone)]
pub enum ParseError {
    Unclosed,
    InvalidImport(String),
}

/* ----------- *\
  PREFIX
    ▼
   {@ EXPR }
   ▲       ▲
   DELIMITER
\* ----------- */
pub const OPEN_DELIMITER: char = '{';
pub const CLOSE_DELIMITER: char = '}';

pub const UNESCAPED_PREFIX: char = '@';
pub const OPEN_BLOCK_PREFIX: char = '!';
pub const CLOSE_BLOCK_PREFIX: char = '/';
pub const BLOCK_PREFIX: char = '#';
pub const IMPORT_PREFIX: char = '$';

/*
  https://regex101.com/r/oP95kB/1

  Matches:
  func1, Func2 as f2, a from "./file.ts" as DEFAULT
  "./file.ts" as DEFAULT
  func1, Func2 as f2, a from "./file.ts"
  a from "./file.ts"
  b as c, a from "./file.ts"
  a as b from "./file.ts"
*/
const IMPORT_REGEX: &str = r#"(?:((?:(?:,\s*)?[a-zA-Z_$][a-zA-Z0-9_$]*(?:\s+as\s+[a-zA-Z_$][a-zA-Z0-9_$]*)?)+) from\s+)?"([^"]+)"(?:\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?"#;

impl HtmlPart {
    #[inline]
    pub fn from_prefix(prefix: char, content: String) -> HtmlPart {
        match prefix {
            UNESCAPED_PREFIX => Self::Unescaped(content),
            OPEN_BLOCK_PREFIX => Self::Block(content),
            CLOSE_BLOCK_PREFIX => Self::Block(content),
            BLOCK_PREFIX => Self::Block(content),
            IMPORT_PREFIX => Self::Import(content),
            _ => Self::Eval(content),
        }
    }

    #[inline]
    pub fn is_eval(prefix: char) -> bool {
        match prefix {
            UNESCAPED_PREFIX | OPEN_BLOCK_PREFIX | CLOSE_BLOCK_PREFIX | BLOCK_PREFIX
            | IMPORT_PREFIX => false,
            _ => true,
        }
    }
}

pub fn search_delimiter(delimiter: char, last_index: usize, content: &String) -> Option<usize> {
    let mut tmp_index = last_index;
    while tmp_index < content.len() {
        let index = match content[tmp_index..].find(delimiter) {
            Some(0) => return Some(last_index),
            None => return None,
            Some(index) => index,
        };

        // When the char before the delimiter is `\`
        // then skip it
        let before_char = content.chars().nth(index - 1);
        if before_char == Some('\\') {
            let delimiter_len = 1;
            tmp_index = index + delimiter_len;
            continue;
        }

        return Some(last_index + index);
    }

    None
}

pub fn handle_expression(expr: &str) -> Option<HtmlPart> {
    let expr = expr.trim();
    if expr.len() == 0 {
        return None;
    }

    let prefix = expr.chars().nth(0).unwrap();

    // The CLOSE_BLOCK_PREFIX has special output,
    // always is a block with "}" as content
    if prefix == CLOSE_BLOCK_PREFIX {
        let content = if expr.len() > 1 {
            format!("{} {} {}", "}", expr[1..].trim_start(), "{")
        } else {
            "}".to_string()
        };

        return Some(HtmlPart::from_prefix(prefix, content));
    }

    let expr = if HtmlPart::is_eval(prefix) {
        expr
    } else {
        &expr[1..].trim_start()
    };

    if expr.len() == 0 {
        return None;
    }

    let mut expr = expr.to_string();

    if prefix == OPEN_BLOCK_PREFIX {
        expr += " {";
    }

    Some(HtmlPart::from_prefix(prefix, expr))
}

pub fn normalize(dirty_parts: Vec<HtmlPart>) -> Result<DynamicHtml, ParseError> {
    let regex = Regex::new(IMPORT_REGEX).unwrap();
    let mut imports: Vec<HtmlImportPart> = vec![];
    let mut parts: Vec<HtmlPart> = vec![];

    for part in dirty_parts.iter() {
        match part {
            HtmlPart::Import(content) => {
                let captures = regex.captures(content);
                let captures = match captures {
                    Some(expr) => expr,
                    None => return Err(ParseError::InvalidImport(content.clone())),
                };

                let (import, filename, default_name) =
                    (captures.get(1), captures.get(2), captures.get(3));
                let import = match import {
                    Some(imports) => Some(imports.as_str().to_string()),
                    None => None,
                };
                let default_name = match default_name {
                    Some(default_name) => Some(default_name.as_str().to_string()),
                    None => None,
                };

                let import_part = HtmlImportPart {
                    content: content.to_string(),
                    default_name,
                    filename: filename.unwrap().as_str().to_string(),
                    imports: import,
                };

                imports.push(import_part);
            }
            part => parts.push(part.clone()),
        }
    }

    Ok(DynamicHtml { imports, parts })
}