New sidebar and folder view (#263)

This commit is contained in:
Gregory Schier
2025-10-15 13:46:57 -07:00
committed by GitHub
parent 19c1efc73e
commit 267cd079ad
80 changed files with 2974 additions and 1450 deletions

View File

@@ -0,0 +1,166 @@
pub fn escape_template(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
// Check if we're at "${["
if i + 2 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' && chars[i + 2] == '[' {
// Count preceding backslashes
let mut backslash_count = 0;
let mut j = i;
while j > 0 && chars[j - 1] == '\\' {
backslash_count += 1;
j -= 1;
}
// If odd number of backslashes, the $ is escaped
// If even number (including 0), the $ is not escaped
let already_escaped = backslash_count % 2 == 1;
if already_escaped {
// Already escaped, just add the current character
result.push(chars[i]);
} else {
// Not escaped, add backslash before $
result.push('\\');
result.push(chars[i]);
}
} else {
result.push(chars[i]);
}
i += 1;
}
result
}
pub fn unescape_template(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
// Check if we're at "\${["
if i + 3 < chars.len()
&& chars[i] == '\\'
&& chars[i + 1] == '$'
&& chars[i + 2] == '{'
&& chars[i + 3] == '['
{
// Count preceding backslashes (before the current backslash)
let mut backslash_count = 0;
let mut j = i;
while j > 0 && chars[j - 1] == '\\' {
backslash_count += 1;
j -= 1;
}
// If even number of preceding backslashes, this backslash escapes the $
// If odd number, this backslash is itself escaped
let escapes_dollar = backslash_count % 2 == 0;
if escapes_dollar {
// Skip the backslash, just add the $
result.push(chars[i + 1]);
i += 1; // Skip the backslash
} else {
// This backslash is escaped itself, keep it
result.push(chars[i]);
}
} else {
result.push(chars[i]);
}
i += 1;
}
result
}
#[cfg(test)]
mod tests {
use crate::escape::{escape_template, unescape_template};
#[test]
fn test_escape_simple() {
let input = r#"${[foo]}"#;
let expected = r#"\${[foo]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_already_escaped() {
let input = r#"\${[bar]}"#;
let expected = r#"\${[bar]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_double_backslash() {
let input = r#"\\${[bar]}"#;
let expected = r#"\\\${[bar]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_escape_with_surrounding_text() {
let input = r#"text ${[var]} more"#;
let expected = r#"text \${[var]} more"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_preserve_already_escaped() {
let input = r#"already \${[escaped]}"#;
let expected = r#"already \${[escaped]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_multiple_occurrences() {
let input = r#"${[one]} and ${[two]}"#;
let expected = r#"\${[one]} and \${[two]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_mixed_escaped_and_unescaped() {
let input = r#"mixed \${[esc]} and ${[unesc]}"#;
let expected = r#"mixed \${[esc]} and \${[unesc]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_unescape_simple() {
let input = r#"\${[foo]}"#;
let expected = r#"${[foo]}"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_with_text() {
let input = r#"text \${[var]} more"#;
let expected = r#"text ${[var]} more"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_multiple() {
let input = r#"\${[one]} and \${[two]}"#;
let expected = r#"${[one]} and ${[two]}"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_double_backslash() {
let input = r#"\\\${[bar]}"#;
let expected = r#"\\${[bar]}"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_plain_text() {
let input = r#"${[foo]}"#;
let expected = r#"${[foo]}"#;
assert_eq!(unescape_template(input), expected);
}
}

View File

@@ -1,7 +1,8 @@
pub mod error;
pub mod escape;
pub mod format;
pub mod parser;
pub mod renderer;
pub mod error;
pub mod wasm;
pub use parser::*;

View File

@@ -170,7 +170,13 @@ impl Parser {
let start_pos = self.pos;
while self.pos < self.chars.len() {
if self.match_str("${[") {
if self.match_str(r#"\\"#) {
// Skip double-escapes so we don't trigger our own escapes in the next case
self.curr_text += r#"\\"#;
} else if self.match_str(r#"\${["#) {
// Unescaped template syntax so we treat it as a string
self.curr_text += "${[";
} else if self.match_str("${[") {
let start_curr = self.pos;
if let Some(t) = self.parse_tag()? {
self.push_token(t);
@@ -490,6 +496,39 @@ mod tests {
use crate::error::Result;
use crate::*;
#[test]
fn escaped() -> Result<()> {
let mut p = Parser::new(r#"\${[ foo ]}"#);
assert_eq!(
p.parse()?.tokens,
vec![
Token::Raw {
text: "${[ foo ]}".to_string()
},
Token::Eof
]
);
Ok(())
}
#[test]
fn escaped_tricky() -> Result<()> {
let mut p = Parser::new(r#"\\${[ foo ]}"#);
assert_eq!(
p.parse()?.tokens,
vec![
Token::Raw {
text: r#"\\"#.to_string()
},
Token::Tag {
val: Val::Var { name: "foo".into() }
},
Token::Eof
]
);
Ok(())
}
#[test]
fn var_simple() -> Result<()> {
let mut p = Parser::new("${[ foo ]}");

View File

@@ -1,5 +1,5 @@
use crate::error::Result;
use crate::Parser;
use crate::{escape, Parser};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
@@ -7,4 +7,16 @@ use wasm_bindgen::JsValue;
pub fn parse_template(template: &str) -> Result<JsValue> {
let tokens = Parser::new(template).parse()?;
Ok(serde_wasm_bindgen::to_value(&tokens).unwrap())
}
}
#[wasm_bindgen]
pub fn escape_template(template: &str) -> Result<JsValue> {
let escaped = escape::escape_template(template);
Ok(serde_wasm_bindgen::to_value(&escaped).unwrap())
}
#[wasm_bindgen]
pub fn unescape_template(template: &str) -> Result<JsValue> {
let escaped = escape::unescape_template(template);
Ok(serde_wasm_bindgen::to_value(&escaped).unwrap())
}