diff options
| author | Mike Buland <mike@xagasoft.com> | 2026-06-04 15:41:42 -0700 |
|---|---|---|
| committer | Mike Buland <mike@xagasoft.com> | 2026-06-04 15:41:42 -0700 |
| commit | 057bd8cb2d4a1539701d65489fe647b96a538e98 (patch) | |
| tree | 28fad08a8e9f3dbd7d6b1823b36a574b721ab058 | |
| download | crimtag-057bd8cb2d4a1539701d65489fe647b96a538e98.tar.gz crimtag-057bd8cb2d4a1539701d65489fe647b96a538e98.tar.bz2 crimtag-057bd8cb2d4a1539701d65489fe647b96a538e98.tar.xz crimtag-057bd8cb2d4a1539701d65489fe647b96a538e98.zip | |
Start of my new templating library.
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Cargo.lock | 7 | ||||
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | src/lexer.rs | 258 | ||||
| -rw-r--r-- | src/lib.rs | 75 |
5 files changed, 349 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffa72a8 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | /target | ||
| 2 | .*.swp | ||
| 3 | .*.swo | ||
diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f7e6686 --- /dev/null +++ b/Cargo.lock | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | # This file is automatically @generated by Cargo. | ||
| 2 | # It is not intended for manual editing. | ||
| 3 | version = 4 | ||
| 4 | |||
| 5 | [[package]] | ||
| 6 | name = "crimtag" | ||
| 7 | version = "0.1.0" | ||
diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ee022ca --- /dev/null +++ b/Cargo.toml | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | [package] | ||
| 2 | name = "crimtag" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2024" | ||
| 5 | |||
| 6 | [dependencies] | ||
diff --git a/src/lexer.rs b/src/lexer.rs new file mode 100644 index 0000000..d6806d8 --- /dev/null +++ b/src/lexer.rs | |||
| @@ -0,0 +1,258 @@ | |||
| 1 | use std::iter::Iterator; | ||
| 2 | //use core::error::Error; | ||
| 3 | use std::str::CharIndices; | ||
| 4 | |||
| 5 | #[derive(Debug)] | ||
| 6 | pub enum Symbol<'a> { | ||
| 7 | StartFlat, | ||
| 8 | StartPoint, | ||
| 9 | EndFlat, | ||
| 10 | EndPoint, | ||
| 11 | Fragment, | ||
| 12 | Section, | ||
| 13 | Output, | ||
| 14 | Equals, | ||
| 15 | Token(&'a str), | ||
| 16 | Literal(&'a str), | ||
| 17 | Text(&'a str), | ||
| 18 | Error{ line: u32, row: u32, what: String }, | ||
| 19 | EOS, | ||
| 20 | } | ||
| 21 | |||
| 22 | enum Mode { | ||
| 23 | Text, | ||
| 24 | InTag, | ||
| 25 | } | ||
| 26 | |||
| 27 | pub struct Lexer<'a> { | ||
| 28 | data: &'a str, | ||
| 29 | chars: CharIndices<'a>, | ||
| 30 | cur: [Option<(usize, char)>;2], | ||
| 31 | icur: usize, | ||
| 32 | mode: Mode, | ||
| 33 | line: u32, | ||
| 34 | row: u32, | ||
| 35 | } | ||
| 36 | |||
| 37 | impl<'a> Lexer<'a> { | ||
| 38 | pub fn new( data: &'a str ) -> Lexer<'a> { | ||
| 39 | let mut chars = data.char_indices(); | ||
| 40 | let cur = chars.next(); | ||
| 41 | let cur2 = chars.next(); | ||
| 42 | //println!(" - cur: {:?}, peek: {:?}", cur, cur2 ); | ||
| 43 | Lexer { | ||
| 44 | data, | ||
| 45 | chars, | ||
| 46 | cur: [cur, cur2], | ||
| 47 | icur: 0, | ||
| 48 | mode: Mode::Text, | ||
| 49 | line: 0, | ||
| 50 | row: 0, | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | fn next(&mut self) -> Option<char> { | ||
| 55 | self.cur[self.icur] = self.chars.next(); | ||
| 56 | self.icur = (self.icur+1)%2; | ||
| 57 | //println!(" - cur: {:?}, peek: {:?}", self.cur(), self.peek() ); | ||
| 58 | if let Some((_,chr)) = self.cur[self.icur] { | ||
| 59 | Some(chr) | ||
| 60 | } else { | ||
| 61 | None | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | fn cur(&self) -> Option<char> { | ||
| 66 | if let Some((_, chr)) = self.cur[self.icur] { | ||
| 67 | Some(chr) | ||
| 68 | } else { | ||
| 69 | None | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | fn cur_index(&self) -> usize { | ||
| 74 | if let Some((idx, _)) = self.cur[self.icur] { | ||
| 75 | idx | ||
| 76 | } else { | ||
| 77 | self.chars.offset() | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | fn peek(&self) -> Option<char> { | ||
| 82 | if let Some((_,chr)) = self.cur[(self.icur+1)%2] { | ||
| 83 | Some(chr) | ||
| 84 | } else { | ||
| 85 | None | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | fn peek_index(&self) -> usize { | ||
| 90 | if let Some((idx, _)) = self.cur[(self.icur+1)%2] { | ||
| 91 | idx | ||
| 92 | } else { | ||
| 93 | self.chars.offset() | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | fn error( &self, what: String ) -> Option<Symbol<'a>> { | ||
| 98 | Some(Symbol::Error { | ||
| 99 | line: self.line, | ||
| 100 | row: self.row, | ||
| 101 | what: what | ||
| 102 | }) | ||
| 103 | } | ||
| 104 | |||
| 105 | fn skip_ws( &mut self ) { | ||
| 106 | while self.cur().is_some_and(|x|x.is_whitespace()) { | ||
| 107 | self.next(); | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | fn next_symbol(&mut self) -> Option<Symbol<'a>> { | ||
| 112 | // If we hit the end then we're already done. | ||
| 113 | if self.cur().is_none() { | ||
| 114 | return None; | ||
| 115 | } | ||
| 116 | |||
| 117 | match self.mode { | ||
| 118 | Mode::Text => { | ||
| 119 | if let Some(sym) = self.parse_start_tag() { | ||
| 120 | Some(sym) | ||
| 121 | } else { | ||
| 122 | self.parse_text() | ||
| 123 | } | ||
| 124 | } | ||
| 125 | Mode::InTag => { | ||
| 126 | if let Some(sym) = self.parse_end_tag() { | ||
| 127 | Some(sym) | ||
| 128 | } else { | ||
| 129 | self.parse_token() | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | fn parse_text(&mut self) -> Option<Symbol<'a>> { | ||
| 136 | let start = self.cur_index(); | ||
| 137 | while self.next().is_some() && !self.is_start_tag() { } | ||
| 138 | let end = self.cur_index(); | ||
| 139 | let s = &self.data[start..end]; | ||
| 140 | //println!(" text: >>>{}<<<", s); | ||
| 141 | if start == end { | ||
| 142 | None | ||
| 143 | } else { | ||
| 144 | Some(Symbol::Text(s)) | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | fn parse_token(&mut self) -> Option<Symbol<'a>> { | ||
| 149 | self.skip_ws(); | ||
| 150 | if let Some(chr) = self.cur() && chr == '"' { | ||
| 151 | // Literal string | ||
| 152 | return self.parse_literal_str(); | ||
| 153 | } | ||
| 154 | let start = self.cur_index(); | ||
| 155 | |||
| 156 | while self.next().is_some_and(|ch| !ch.is_whitespace() ) && | ||
| 157 | !self.is_end_tag() {} | ||
| 158 | let end = self.cur_index(); | ||
| 159 | let s = &self.data[start..end]; | ||
| 160 | if start == end { | ||
| 161 | None | ||
| 162 | } else { | ||
| 163 | Some(match s { | ||
| 164 | "fragment" => Symbol::Fragment, | ||
| 165 | "section" => Symbol::Section, | ||
| 166 | "output" => Symbol::Output, | ||
| 167 | _ => Symbol::Token(s) | ||
| 168 | }) | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | fn parse_literal_str(&mut self) -> Option<Symbol<'a>> { | ||
| 173 | if let Some(chr) = self.cur() && chr != '"' { | ||
| 174 | return self.error(format!("Expected '\"' but found '{}'", chr)); | ||
| 175 | } | ||
| 176 | let start = self.peek_index(); | ||
| 177 | while self.next().is_some_and(|chr| chr != '"') { } | ||
| 178 | let end = self.cur_index(); | ||
| 179 | self.next(); | ||
| 180 | let s = &self.data[start..end]; | ||
| 181 | Some(Symbol::Literal(s)) | ||
| 182 | } | ||
| 183 | |||
| 184 | fn is_start_tag(&mut self) -> bool { | ||
| 185 | if let Some(cur) = self.cur() && (cur == '[' || cur == '<') && | ||
| 186 | let Some(peek) = self.peek() && peek == '|' { | ||
| 187 | true | ||
| 188 | } else { | ||
| 189 | false | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | fn parse_start_tag(&mut self) -> Option<Symbol<'a>> { | ||
| 194 | match self.cur() { | ||
| 195 | Some('[') => { | ||
| 196 | if let Some(p) = self.peek() && p == '|' { | ||
| 197 | self.next(); self.next(); | ||
| 198 | self.mode = Mode::InTag; | ||
| 199 | Some(Symbol::StartFlat) | ||
| 200 | } else { | ||
| 201 | None | ||
| 202 | } | ||
| 203 | } | ||
| 204 | Some('<') => { | ||
| 205 | if let Some(p) = self.peek() && p == '|' { | ||
| 206 | self.next(); self.next(); | ||
| 207 | self.mode = Mode::InTag; | ||
| 208 | Some(Symbol::StartPoint) | ||
| 209 | } else { | ||
| 210 | None | ||
| 211 | } | ||
| 212 | } | ||
| 213 | _ => { | ||
| 214 | None | ||
| 215 | } | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | fn is_end_tag(&mut self) -> bool { | ||
| 220 | if let Some(cur) = self.cur() && cur == '|' && | ||
| 221 | let Some(peek) = self.peek() && (peek == '>' || peek == ']') { | ||
| 222 | true | ||
| 223 | } else { | ||
| 224 | false | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | fn parse_end_tag(&mut self) -> Option<Symbol<'a>> { | ||
| 229 | if let Some(chr) = self.cur() && chr == '|' { | ||
| 230 | match self.peek() { | ||
| 231 | Some(']') => { | ||
| 232 | self.next(); self.next(); | ||
| 233 | self.mode = Mode::Text; | ||
| 234 | Some(Symbol::EndFlat) | ||
| 235 | } | ||
| 236 | Some('>') => { | ||
| 237 | self.next(); self.next(); | ||
| 238 | self.mode = Mode::Text; | ||
| 239 | Some(Symbol::EndPoint) | ||
| 240 | } | ||
| 241 | _ => { | ||
| 242 | None | ||
| 243 | } | ||
| 244 | } | ||
| 245 | } else { | ||
| 246 | None | ||
| 247 | } | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | impl<'a> Iterator for Lexer<'a> { | ||
| 252 | type Item = Symbol<'a>; | ||
| 253 | |||
| 254 | fn next(&mut self) -> Option<Self::Item> { | ||
| 255 | self.next_symbol() | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..57f777e --- /dev/null +++ b/src/lib.rs | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | use std::path::{PathBuf,Path}; | ||
| 2 | use std::collections::HashMap; | ||
| 3 | use core::error::Error; | ||
| 4 | |||
| 5 | mod lexer; | ||
| 6 | |||
| 7 | pub struct Tx { | ||
| 8 | fragments: HashMap<String, Fragment>, | ||
| 9 | } | ||
| 10 | |||
| 11 | enum FragmentSource { | ||
| 12 | File { | ||
| 13 | path: PathBuf, | ||
| 14 | loaded: usize, | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 18 | struct Fragment { | ||
| 19 | name: String, | ||
| 20 | source: FragmentSource, | ||
| 21 | sections: HashMap<String, Section>, | ||
| 22 | } | ||
| 23 | |||
| 24 | struct Section { | ||
| 25 | name: String, | ||
| 26 | ast_root: Token, | ||
| 27 | } | ||
| 28 | |||
| 29 | type Properties = HashMap<String, String>; | ||
| 30 | |||
| 31 | enum Token { | ||
| 32 | Root(Vec<Token>), | ||
| 33 | Fragment(String,Properties,Vec<Token>), | ||
| 34 | Section(String,Properties,Vec<Token>), | ||
| 35 | Text(String), | ||
| 36 | |||
| 37 | } | ||
| 38 | |||
| 39 | struct State { | ||
| 40 | } | ||
| 41 | |||
| 42 | impl Tx { | ||
| 43 | pub fn new() -> Self { | ||
| 44 | Self { | ||
| 45 | fragments: HashMap::new(), | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | pub fn load(path: &Path) -> Result<(),Box::<dyn Error>> { | ||
| 50 | Ok(()) | ||
| 51 | } | ||
| 52 | |||
| 53 | pub fn parse(data: String) -> Result<(),Box::<dyn Error>> { | ||
| 54 | Ok(()) | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | #[cfg(test)] | ||
| 59 | mod tests { | ||
| 60 | use super::*; | ||
| 61 | |||
| 62 | #[test] | ||
| 63 | fn lexing() { | ||
| 64 | let data = r#"Leading comment: [|fragment "basic"|>Hello there <|fragment|] Trailing text"#; | ||
| 65 | let ll = lexer::Lexer::new( &data ); | ||
| 66 | for sym in ll { | ||
| 67 | if let lexer::Symbol::Error{line,row,what} = sym { | ||
| 68 | println!("Error {}:{}: {}", line, row, what ); | ||
| 69 | break; | ||
| 70 | } else { | ||
| 71 | println!("Symbol: {:?}", sym ); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | } | ||
