#![allow(dead_code)]
extern crate alloc;
use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
use alloc::string::String;
use core::fmt;
use core::fmt::Write;
pub use expry::{BytecodeVec,BytecodeRef,EncodedValueVec,EncodedValueRef,NoCustomFuncs};
use expry::*;
const MAX_TEMPLATE_RECURSION: usize = 64;
const MAX_TEMPLATE_ARGS: u64 = 32;
#[derive(Copy, Clone)]
pub enum HairyParserError<'a> {
Parser(&'a str),
Expr(CompileErrorDescription<'a>),
Type(&'a str),
Eval(EvalError<'a>),
Other(&'a str),
}
#[derive(Clone, Debug)]
pub struct HairyCompileError<'a> {
expr: &'a str,
error: HairyParserError<'a>,
start: usize, end: usize, extra: Option<(usize, usize)>,
line_context: Option<LineContext>,
extra_line_no: u32,
}
impl<'a> HairyCompileError<'a> {
fn get_line_context(&mut self) -> &LineContext {
if self.line_context.is_none() {
self.line_context = Some(LineContext::new(self.expr));
}
self.line_context.as_ref().unwrap()
}
}
impl<'a> core::fmt::Display for HairyCompileError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} [{}-{}]", self.error, self.start, self.end)
}
}
impl<'a> From<EncodingError> for HairyParserError<'a> {
fn from(_: EncodingError) -> Self {
HairyParserError::Other("internal encoding error")
}
}
impl<'a> fmt::Debug for HairyParserError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl<'a> core::fmt::Display for HairyParserError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HairyParserError::Expr(err) => write!(f, "{} (during parsing of the expression)", err),
HairyParserError::Parser(s) => write!(f, "{}", s),
HairyParserError::Other(s) => write!(f, "{}", s),
HairyParserError::Eval(s) => write!(f, "{}", s),
HairyParserError::Type(s) => write!(f, "{}", s),
}
}
}
type Expr<'a> = &'a str;
type Text<'a> = &'a str;
type Name<'a> = &'a str;
type Bytes<'a> = &'a [u8];
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum HairyToken<'a> {
EndOfInput(),
Text(Text<'a>),
Loop(Expr<'a>, usize, Name<'a>, Name<'a>), Conditional(Option<Name<'a>>, Expr<'a>, usize), ElseConditional(Option<Name<'a>>, Expr<'a>, usize), Else(),
BlockEnd(Name<'a>),
Eval(Expr<'a>, usize, Bytes<'a>), CallTemplate(Name<'a>, bool, usize, &'a [(Expr<'a>, usize)]), TemplateDef(Name<'a>, &'a [(Name<'a>, Text<'a>, usize)]),
Arguments(&'a [(Name<'a>, Text<'a>, usize)]),
Inline(Name<'a>, Text<'a>, usize),
}
pub struct TemplateTagContentSpanner {
prev_was_escape: bool,
string: bool,
nested: u32,
}
impl Spanner for TemplateTagContentSpanner {
fn next(&mut self, b: char) -> bool {
if !self.prev_was_escape && !self.string && b == '{' {
self.nested += 1;
}
if !self.prev_was_escape && !self.string && b == '}' {
if self.nested == 0 {
return false;
}
self.nested -= 1;
}
if !self.prev_was_escape && b == '"' {
self.string = !self.string;
}
self.prev_was_escape = !self.prev_was_escape && b == '\\';
true
}
fn valid(&mut self, _len: usize) -> bool {
self.nested == 0 && !self.string && !self.prev_was_escape
}
}
impl TemplateTagContentSpanner {
pub fn new() -> Self {
Self {
prev_was_escape: false,
string: false,
nested: 0,
}
}
}
impl Default for TemplateTagContentSpanner {
fn default() -> Self {
Self::new()
}
}
pub struct ExprySpanner<'a> {
prev_was_escape: bool,
string: bool,
nested: u32,
stop: &'a [char],
}
impl<'a> Spanner for ExprySpanner<'a> {
fn next(&mut self, b: char) -> bool {
if !self.prev_was_escape && !self.string && (b == '{' || b == '[' || b == '(') {
self.nested += 1;
}
if self.nested == 0 && !self.prev_was_escape && !self.string && self.stop.contains(&b) {
return false;
}
if !self.prev_was_escape && !self.string && (b == '}' || b == ']' || b == ')') {
if self.nested == 0 {
return false;
}
self.nested -= 1;
}
if !self.prev_was_escape && b == '"' {
self.string = !self.string;
}
self.prev_was_escape = !self.prev_was_escape && b == '\\';
true
}
fn valid(&mut self, len: usize) -> bool {
len > 0 && self.nested == 0 && !self.string && !self.prev_was_escape
}
}
impl<'a> ExprySpanner<'a> {
pub fn new(stop: &'a [char]) -> Self {
Self {
prev_was_escape: false,
string: false,
nested: 0,
stop,
}
}
}
impl Default for ExprySpanner<'static> {
fn default() -> Self {
Self::new(&[])
}
}
fn find_subsequence<T>(haystack: &[T], needle: &[T]) -> Option<usize> where for<'a> &'a [T]: PartialEq {
haystack.windows(needle.len()).position(|window| window == needle)
}
fn hairy_tokenize<'b>(
reader: &mut &'b str,
context: &mut ParserContext<'_,'b,'_,'_>,
) -> Result<(HairyToken<'b>,usize), (HairyParserError<'b>,usize,usize)> {
const EXPECT_HAIRY_CLOSE : HairyParserError = HairyParserError::Parser("expecting closing `}}` after a non empty expression");
fn read_enters(reader: &mut &str) -> usize {
let mut retval = 0;
if reader.accept("\r") {
retval += 1;
}
if reader.accept("\n") {
retval += 1;
}
retval
}
while reader.accept("{{!") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
embedded_spanner.span(reader);
if !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
read_enters(reader);
}
let remaining = reader.len();
if reader.is_empty() {
return Ok((HairyToken::EndOfInput(), remaining));
}
let variable_matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
if reader.accept("{{for ") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let expr = embedded_spanner.span(reader).map_or("", |x| x);
if expr.is_empty() || !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
let content_end = reader.len() + 2;
let content_end_offset = 2 + read_enters(reader);
const IN_LEN : usize = 4;
if let Some((variable, expr)) = expr.split_once(" in ") {
let mut variable = variable.trim_start();
let mut index_variable = "";
let mut content_variable = "";
if strparse(variable).accept('(').span(is_space, 0).span(variable_matcher, 1).matched(&mut index_variable).span(is_space, 0).accept(",").span(is_space, 0).span(variable_matcher, 1).matched(&mut content_variable).span(is_space, 0).accept(')').valid(&mut variable) {
return Ok((HairyToken::Loop(expr, content_end_offset, index_variable, content_variable), remaining));
}
if variable.trim().contains(|x| !variable_matcher(x)) {
const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("variable name may only consists of alphanumeric chars, and `$` and `_`");
return Err((EXPECT_HAIRY_VARIABLE, variable.len() + IN_LEN + expr.len() + content_end, IN_LEN + expr.len() + content_end));
}
return Ok((HairyToken::Loop(expr, content_end_offset, "", variable), remaining));
} else {
const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("expected variable name (indicated with `{{for variable in expr}}`)");
return Err((EXPECT_HAIRY_VARIABLE, expr.len() + content_end, content_end));
}
}
let elseif = reader.accept("{{elseif ");
if elseif || reader.accept("{{if ") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let expr = embedded_spanner.span(reader).map_or("", |x| x);
if expr.is_empty() || !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
let content_end = reader.len() + 2;
let content_end_offset = 2 + read_enters(reader);
if let Some(expr) = expr.strip_prefix("let ") {
const ASSIGNMENT_LEN : usize = 4;
if let Some((variable, expr)) = expr.split_once(" = ") {
if variable.trim().contains(|x| !variable_matcher(x)) {
const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("variable name may only consists of alphanumeric chars, and `$` and `_`");
return Err((EXPECT_HAIRY_VARIABLE, variable.len() + ASSIGNMENT_LEN + expr.len() + content_end, ASSIGNMENT_LEN + expr.len() + content_end));
}
if elseif {
return Ok((HairyToken::ElseConditional(Some(variable), expr, content_end_offset),remaining));
} else {
return Ok((HairyToken::Conditional(Some(variable), expr, content_end_offset),remaining));
}
} else {
const EXPECT_HAIRY_VARIABLE : HairyParserError = HairyParserError::Parser("expected variable name (indicated with `{{if let variable = expr}}`)");
return Err((EXPECT_HAIRY_VARIABLE, expr.len() + content_end, content_end));
}
} else if elseif {
return Ok((HairyToken::ElseConditional(None, expr, content_end_offset),remaining));
} else {
return Ok((HairyToken::Conditional(None, expr, content_end_offset),remaining));
}
}
if reader.accept("{{else}}") {
read_enters(reader);
return Ok((HairyToken::Else(),remaining));
}
if reader.accept("{{end") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let expr = embedded_spanner.span(reader).map_or("", |x| x);
if !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
while read_enters(reader) > 0 {
}
return Ok((HairyToken::BlockEnd(expr),remaining));
}
if reader.accept("{{inline ") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let expr = embedded_spanner.span(reader).map_or("", |x| x);
if expr.is_empty() || !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
let content_end = reader.len() + 2;
let content_end_offset = 2 + read_enters(reader);
let check = |name: &str, extra_len: usize| -> Result<(),(HairyParserError, usize, usize)> {
let matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
if name.trim().contains(|x| !matcher(x)) {
const HAIRY_ERROR : HairyParserError = HairyParserError::Parser("template name may only consists of alphanumeric chars, and `$` and `_`");
return Err((HAIRY_ERROR, name.len() + extra_len + content_end, extra_len + content_end));
}
Ok(())
};
if let Some((name, expr)) = expr.split_once('=') {
check(name, 1 + expr.len())?;
let name = name.trim();
return Ok((HairyToken::Inline(name, expr, expr.len() + content_end_offset),remaining));
} else {
return Err((HairyParserError::Parser("expecting = after variable name"), expr.len() + content_end, content_end));
}
}
if reader.accept("{{arguments") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let mut expr = embedded_spanner.span(reader).map_or("", |x| x);
if expr.is_empty() || !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
let content_end = reader.len() + 2;
let content_end_offset = 2 + read_enters(reader);
let mut args = ScopedArrayBuilder::new(context.allocator);
expr = expr.trim_start();
while let Some(variable) = expr.span_fn(&mut |c| c == '_' || c.is_ascii_alphanumeric()) {
if !expr.accept(":") {
args.push((variable.trim(), "*", expr.len() + content_end_offset));
if !expr.accept(",") {
break;
}
expr = expr.trim_start();
continue;
} else {
let mut embedded_spanner = ExprySpanner::new(&[',',')']);
if let Some(type_spec) = embedded_spanner.span(&mut expr) {
args.push((variable.trim(), type_spec, expr.len() + content_end_offset));
if !expr.accept(",") {
break;
}
expr = expr.trim_start();
} else {
return Err((HairyParserError::Parser("expecting value after label"), expr.len() + content_end, content_end));
}
}
}
let args = args.build();
return Ok((HairyToken::Arguments(args),remaining));
}
if reader.accept("{{define ") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let expr = embedded_spanner.span(reader).map_or("", |x| x);
if expr.is_empty() || !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
let content_end = reader.len() + 2;
let content_end_offset = 2 + read_enters(reader);
let check = |name: &str, extra_len: usize| -> Result<(),(HairyParserError, usize, usize)> {
let matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
if name.trim().contains(|x| !matcher(x)) {
const HAIRY_ERROR : HairyParserError = HairyParserError::Parser("template name may only consists of alphanumeric chars, and `$` and `_`");
return Err((HAIRY_ERROR, name.len() + extra_len + content_end, extra_len + content_end));
}
Ok(())
};
if let Some((name, mut expr)) = expr.split_once('(') {
check(name, 1 + expr.len())?;
let mut args = ScopedArrayBuilder::new(context.allocator);
expr = expr.trim_start();
while let Some(variable) = expr.span_fn(&mut |c| c == '_' || c.is_ascii_alphanumeric()) {
if !expr.accept(":") {
args.push((variable.trim(), "*", expr.len() + content_end_offset));
if !expr.accept(",") {
break;
}
expr = expr.trim_start();
continue;
} else {
let mut embedded_spanner = ExprySpanner::new(&[',',')']);
if let Some(type_spec) = embedded_spanner.span(&mut expr) {
args.push((variable.trim(), type_spec, expr.len() + content_end_offset));
if !expr.accept(",") {
break;
}
expr = expr.trim_start();
} else {
return Err((HairyParserError::Parser("expecting value after label"), expr.len() + content_end, content_end));
}
}
}
if !expr.accept(")") {
return Err((HairyParserError::Parser("expecting ')' after arguments"), expr.len() + content_end, content_end));
}
let args = args.build();
return Ok((HairyToken::TemplateDef(name.trim(), args),remaining));
}
check(expr, 0)?;
return Ok((HairyToken::TemplateDef(expr.trim(), &[]),remaining));
}
if reader.accept("{{call ") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let mut expr = embedded_spanner.span(reader).map_or("", |x| x);
if expr.is_empty() || !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
let content_end = reader.len() + 2;
let content_end_offset = 2 + read_enters(reader);
let dynamic = expr.accept("(");
let mut matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
if let Some(name) = if dynamic { ExprySpanner::default().span(&mut expr) } else { expr.span_fn(&mut matcher) } {
if dynamic && !expr.accept(")") {
return Err((HairyParserError::Parser("expecting ')' to close dynamic expression for name"), expr.len() + content_end, content_end));
}
if !expr.accept("(") {
return Err((HairyParserError::Parser("expecting '(' for arguments"), expr.len() + content_end, content_end));
}
let offset = 1 + expr.len() + content_end_offset;
let mut args = ScopedArrayBuilder::new(context.allocator);
expr = expr.trim_start();
while let Some(expression) = ExprySpanner::new(&[',',')']).span(&mut expr) {
args.push((expression.trim_start(), expr.len() + content_end_offset));
if !expr.accept(",") {
break;
}
expr = expr.trim_start();
}
if !expr.accept(")") {
return Err((HairyParserError::Parser("expecting ')' after arguments"), expr.len() + content_end, content_end));
}
let args = args.build();
return Ok((HairyToken::CallTemplate(name.trim(), dynamic, offset + usize::from(dynamic), args),remaining));
}
let matcher = |c: char| c == '$' || c == '_' || c == '\\' || c.is_ascii_alphanumeric();
if expr.trim().contains(|x| !matcher(x)) {
const HAIRY_ERROR : HairyParserError = HairyParserError::Parser("template name may only consists of alphanumeric chars, and `$` and `_` (and start with `*` or `**`)");
return Err((HAIRY_ERROR, expr.len() + content_end, content_end));
}
return Ok((HairyToken::CallTemplate(expr, false, content_end_offset, &[]), remaining));
}
if reader.accept("{{=") {
let mut embedded_spanner = TemplateTagContentSpanner::new();
let expr = embedded_spanner.span(reader).map_or("", |x| x);
if expr.is_empty() || !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
let content_end_offset = 2;
const ESCAPE_MODE_LEN : usize = 1;
if let Some((expr, escape_mode)) = expr.rsplit_once(':') {
if escape_mode.chars().all(|c| c.is_ascii_alphanumeric()) {
return Ok((HairyToken::Eval(expr, escape_mode.len()+ESCAPE_MODE_LEN+content_end_offset, escape_mode.as_bytes()),remaining));
}
}
return Ok((HairyToken::Eval(expr, content_end_offset, b""),remaining));
}
if reader.accept("{{") {
const EXPECT_HAIRY_COMMAND : HairyParserError = HairyParserError::Parser("expected a command after an hairy opening tag: if, else, end, for, call, define, arguments, inline, =");
return Err((EXPECT_HAIRY_COMMAND, remaining, reader.len()));
}
let mut retval = ScopedStringBuilder::new(context.allocator);
loop {
let mut start = 0;
let mut found;
loop {
found = reader[start..].find('{').map(|x| x+start);
if let Some(pos) = found {
if pos >= reader.len()-1 || &reader[pos..pos+2] != "{{" {
start = pos+1;
continue;
}
}
break;
}
match found {
None => {
let extra = &reader[..];
if !extra.is_empty() {
retval.write_str(extra).ok();
}
*reader = &reader[0..0];
return Ok((HairyToken::Text(retval.build()),remaining));
},
Some(n) => {
let extra = &reader[0..n];
*reader = &reader[n..];
if !extra.ends_with('\\') {
if !extra.is_empty() {
retval.write_str(extra).ok();
}
return Ok((HairyToken::Text(retval.build()),remaining));
}
let extra = &extra[..extra.len()-1];
if !extra.is_empty() {
retval.write_str(extra).ok();
}
let reader_saved = *reader;
*reader = &reader[2..]; let mut embedded_spanner = ExprySpanner::default();
if let Some(expr) = embedded_spanner.span(reader) {
let expr = &reader_saved[0..expr.len()+4]; retval.write_str(expr).ok();
}
if !reader.accept("}}") {
return Err((EXPECT_HAIRY_CLOSE, reader.len(), reader.len()));
}
},
}
}
}
#[allow(clippy::upper_case_acronyms)]
type AST<'a> = Vec<HairyCommand<'a>>;
type LineNo = u32;
type ColumnNo = u32;
type SourceContext = (LineNo, ColumnNo);
#[derive(PartialEq, Eq, Clone, Copy)]
enum ResolveTemplate<'a> {
Static(Name<'a>),
DynamicName(BytecodeRef<'a>),
DynamicBody(BytecodeRef<'a>),
}
#[derive(PartialEq, Clone)]
enum HairyCommand<'a> {
Text(Bytes<'a>),
Loop(BytecodeRef<'a>, Name<'a>, Name<'a>, AST<'a>, SourceContext),
Conditional(Vec<(Option<Name<'a>>, BytecodeRef<'a>, AST<'a>, SourceContext)>, AST<'a>),
Eval(BytecodeRef<'a>, Bytes<'a>, SourceContext), CallTemplate(ResolveTemplate<'a>, Vec<(BytecodeRef<'a>, ExpryType)>, SourceContext),
}
impl<'a> fmt::Debug for ResolveTemplate<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResolveTemplate::Static(name) => write!(f, "{}", name),
ResolveTemplate::DynamicName(_) => write!(f, "<dynamic-name>"),
ResolveTemplate::DynamicBody(_) => write!(f, "<dynamic-body>"),
}
}
}
impl<'a> fmt::Debug for HairyCommand<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HairyCommand::Text(s) => write!(f, "\"{}\"", String::from_utf8_lossy(s)),
HairyCommand::Eval(expr, mode, _) => write!(f, "{{{{{:?}:{:?}}}}}", expr, mode),
HairyCommand::Conditional(conditions, else_body) => {
for (i,(variable, condition, body, _source_context)) in conditions.iter().enumerate() {
if let Some(variable) = variable {
write!(f, "{{{{{} let {} = {:?}}}}}", if i == 0 { "if" } else { "elseif" }, variable, condition)?;
} else {
write!(f, "{{{{{} {:?}}}}}", if i == 0 { "if" } else { "elseif" }, condition)?;
}
write!(f, "{:?}", body)?;
}
write!(f, "{{{{else}}}}")?;
write!(f, "{:?}", else_body)?;
write!(f, "{{{{end}}}}")
},
HairyCommand::Loop(tag, index_variable, content_variable, body, _) => {
write!(f, "{{{{for ({:?},{:?}) in {:?}}}}}", index_variable, content_variable, tag)?;
write!(f, "{:?}", body)?;
write!(f, "{{{{end}}}}")
},
HairyCommand::CallTemplate(name, defaults, _) => {
write!(f, "{{{{{:?} <- {:?}}}}}", name, defaults)
},
}
}
}
struct Define<'b> {
name: &'b str,
filename: &'b str,
args: Vec<ExpryType>,
body: AST<'b>,
subtemplates: Vec<Define<'b>>,
}
struct CompiledDefine<'b> {
name: &'b [u8],
filename: &'b [u8],
args_count: u64,
lazy_arg_types: &'b [u8], body: BytecodeRef<'b>,
subtemplates: BytecodeRef<'b>,
}
struct ParserContext<'a,'b,'c,'e> where 'c: 'b, 'e: 'b {
line_context: LineContext,
allocator: &'a mut MemoryScope<'c>,
local_defines: Vec<Define<'b>>,
outside_defines: Option<&'a std::collections::BTreeMap<Vec<u8>,Vec<ExpryType>>>,
all_defines_known: bool,
inlines: DecodedObject<'b>,
value_names: Vec<(&'b str, ExpryType)>,
value_count: Option<usize>,
filename: &'b str,
custom_types: &'a std::collections::BTreeMap<Key<'a>, (Vec<ExpryType>, ExpryType)>,
static_custom: &'a dyn CustomFuncs<'e>,
input_len: usize,
escaping: Option<&'a [(usize, &'b [u8])]>,
extra_line_no: u32,
trim_spaces: bool,
}
type HairyParserState<'a,'b,'c,'e> = ParserState<'b,HairyToken<'b>, HairyParserError<'b>, ParserContext<'a,'b,'c,'e>>;
type HairyParserResult<'b,T> = ParserResult<T, HairyParserError<'b>>;
fn hairy_expr<'b,'c,'e>(parser: &'_ mut HairyParserState<'_,'b,'c,'e>) -> HairyParserResult<'b,AST<'b>> where 'c: 'b, 'e: 'b {
let mut retval: AST = Vec::new();
parser.repeat(|parser: &mut _| {
let to_source_context = |pos, parser: &mut HairyParserState| { let (a,b,_,_) = parser.context().line_context.remaining_to_line_info(pos as u32); (a+parser.context().extra_line_no,b) };
match parser.get()? {
(HairyToken::Text(text), _) => {
let text = if parser.context().trim_spaces {
let mut builder = ScopedStringBuilder::new(parser.context().allocator);
let mut last_space = false;
for c in text.chars() {
let this_space = matches!(c, ' ' | '\n' | '\r' | '\t');
if last_space && this_space {
} else if this_space {
builder.push_char(' ');
} else {
builder.push_char(c);
}
last_space = this_space;
}
builder.build()
} else {
text
};
if !text.is_empty() {
retval.push(HairyCommand::Text(text.as_bytes()));
}
Ok(true)
},
(HairyToken::Eval(expr, expr_offset, mut escape_mode), info) => {
let context = parser.context();
let (bytecode,needs_dynamic_eval,return_type,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+expr_offset, e.error_end()+expr_offset), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
if escape_mode.is_empty() {
match return_type {
ExpryType::Any => {},
ExpryType::Null => {},
ExpryType::Int => {},
ExpryType::Float => {},
ExpryType::Double => {},
ExpryType::String => {},
ExpryType::Nullable(x) if matches!(*x, ExpryType::Any | ExpryType::Null | ExpryType::Int | ExpryType::Float | ExpryType::Double | ExpryType::String) => {},
_ => {
let msg = write!(parser.context().allocator, "only numbers and strings can be outputted (in regular escape modes), not {:?}", return_type);
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
},
}
}
let source_context = to_source_context(info.start_to_end_of_input(), parser);
if escape_mode.is_empty() {
if let Some(escaping) = parser.context().escaping {
escape_mode = lookup_escape_mode(escaping, parser.context().input_len - info.start_to_end_of_input());
}
}
if !needs_dynamic_eval && escape_mode == b"none" {
match expry_eval(bytecode, &mut Vec::new(), parser.context().allocator) {
Ok(DecodedValue::String(v)) => {
retval.push(HairyCommand::Text(v));
return Ok(true);
},
other => {
eprintln!("{:?}", other);
},
}
}
retval.push(HairyCommand::Eval(bytecode, escape_mode, source_context));
Ok(true)
},
(HairyToken::CallTemplate(name, dynamic, offset_name, args), info) => {
let mut args_bytecode = Vec::new();
for (expr, offset_expr) in args {
let context = parser.context();
let (bytecode, _, return_type, warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
args_bytecode.push((bytecode, return_type));
}
let source_context = to_source_context(info.start_to_end_of_input(), parser);
let name = name.trim_start_matches(is_space);
if dynamic && name.starts_with('(') {
let context = parser.context();
let (name_bytecode,_needs_dynamic_eval,type_of_body,warnings) = expry_compile_expr_typed(name, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_name, e.error_end()+offset_name), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
if !type_of_body.used_as(&ExpryType::Nullable(Box::new(ExpryType::String))) {
let msg = write!(parser.context().allocator, "expression in '()' should yield a string?, not a {}", type_of_body);
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
}
retval.push(HairyCommand::CallTemplate(ResolveTemplate::DynamicBody(name_bytecode), args_bytecode, source_context));
} else if dynamic {
let context = parser.context();
let (name_bytecode,_needs_dynamic_eval,type_of_name,warnings) = expry_compile_expr_typed(name, Some(&context.inlines), None, &context.value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_name, e.error_end()+offset_name), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
if !type_of_name.used_as(&ExpryType::Nullable(Box::new(ExpryType::String))) {
let msg = write!(parser.context().allocator, "expression in '()' should yield a string?, not a {}", type_of_name);
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
}
retval.push(HairyCommand::CallTemplate(ResolveTemplate::DynamicName(name_bytecode), args_bytecode, source_context));
} else {
if let Some(Define{args,..}) = parser.context().local_defines.iter().find(|x| x.name == name) {
let args_count = args.len();
if args_count != args_bytecode.len() {
let msg = write!(parser.context().allocator, "invocation of {} expected {} arguments, not {}", name, args_count, args_bytecode.len());
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
}
for (i,(expected_type,(_,actual_type))) in (*args).iter().zip(args_bytecode.iter()).enumerate() {
if !actual_type.used_as(expected_type) {
let expected_type = (*expected_type).clone();
let msg = write!(parser.context().allocator, "argument {} to invocation of {} should be {}, but is instead {}", i, name, expected_type, actual_type);
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
}
}
} else if let Some(args) = parser.context().outside_defines.and_then(|x| x.get(name.as_bytes())) {
let args_count = args.len();
if args_count != args_bytecode.len() {
let msg = write!(parser.context().allocator, "invocation of {} expected {} arguments, not {}", name, args_count, args_bytecode.len());
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
}
for (i,(expected_type,(_,actual_type))) in args.iter().zip(args_bytecode.iter()).enumerate() {
if !actual_type.used_as(expected_type) {
let msg = write!(parser.context().allocator, "argument {} to invocation of {} should be {}, but is instead {}", i, name, expected_type, actual_type);
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
}
}
} else if parser.context().all_defines_known {
let msg = write!(parser.context().allocator, "declaration of define '{}' could not be found", name);
return Err(parser.error_other(&info, HairyParserError::Type(msg)));
}
retval.push(HairyCommand::CallTemplate(ResolveTemplate::Static(name), args_bytecode, source_context));
}
Ok(true)
},
(HairyToken::Conditional(variable, expr, offset_expr), info) => {
let value_names = parser.context().value_names.clone();
let context = parser.context();
let (expr_bytecode,_needs_dynamic_eval,return_type,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
let source_context = to_source_context(info.start_to_end_of_input(), parser);
let body;
if let Some(variable) = variable {
let return_type = match return_type {
ExpryType::Nullable(x) => *x,
x => x,
};
parser.context().value_names.push((variable, return_type));
body = hairy_expr(parser);
parser.context().value_names.pop();
} else {
match return_type {
ExpryType::Any => {},
ExpryType::Bool => {},
ExpryType::Nullable(x) if matches!(*x, ExpryType::Bool) => {},
_ => {
let err_msg = write!(parser.context().allocator, "expr should return a bool, not a {} (or use the `if let name = ...` construct)", return_type);
return Err(parser.error_other(&info.bound(offset_expr, offset_expr), HairyParserError::Parser(err_msg)))
},
}
body = hairy_expr(parser);
}
let body = body?;
let mut conditions = vec![(variable, expr_bytecode, body, source_context)];
loop {
match parser.get()? {
(HairyToken::ElseConditional(variable, expr, offset_expr), info) => {
let context = parser.context();
let (expr_bytecode,_needs_dynamic_eval,type_of_bytecode,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
let source_context = to_source_context(info.start_to_end_of_input(), parser);
let body;
if let Some(variable) = variable {
parser.context().value_names.push((variable, type_of_bytecode));
body = hairy_expr(parser);
parser.context().value_names.pop();
} else {
body = hairy_expr(parser);
}
let body = body?;
conditions.push((variable, expr_bytecode, body, source_context));
},
(HairyToken::Else(), _info) => {
let else_body = hairy_expr(parser)?;
match parser.get()? {
(HairyToken::BlockEnd(end_tag), info) => {
if !end_tag.is_empty() && "if" != end_tag.trim() {
let err_msg = write!(parser.context().allocator, "start (if) and end ({}) tag should be the same", end_tag.trim());
return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
}
retval.push(HairyCommand::Conditional(conditions, else_body));
return Ok(true)
},
(token, info) => return Err(parser.error_token(token, info, |_| {
HairyParserError::Parser("expected 'end' of conditional")
})),
}
},
(HairyToken::BlockEnd(end_tag), info) => {
if !end_tag.is_empty() && "if" != end_tag.trim() {
let err_msg = write!(parser.context().allocator, "start (if) and end ({}) tag should be the same", end_tag.trim());
return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
}
retval.push(HairyCommand::Conditional(conditions, Vec::new()));
return Ok(true)
},
(token, info) => return Err(parser.error_token(token, info, |_| {
HairyParserError::Parser("expected 'elseif', 'else', or 'end' of conditional")
})),
}
}
},
(HairyToken::Loop(expr, expr_offset, index_variable, content_variable), info) => {
let value_names = parser.context().value_names.clone();
let context = parser.context();
let (expr_bytecode,_needs_dynamic_eval,type_of_bytecode,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &value_names, context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+expr_offset, e.error_end()+expr_offset), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
let type_of_bytecode = match type_of_bytecode {
ExpryType::Nullable(x) if matches!(*x, ExpryType::Array(_) | ExpryType::Any) => {
if let ExpryType::Array(x) = *x {
*x
} else {
*x
}
},
ExpryType::Array(x) => *x,
ExpryType::Any => type_of_bytecode,
t => {
let err_msg = write!(parser.context().allocator, "expression should return a non-empty array, not a {}", t);
return Err(parser.error_other(&info, HairyParserError::Type(err_msg)))
},
};
let source_context = to_source_context(info.start_to_end_of_input(), parser);
parser.context().value_names.push((index_variable, ExpryType::Int));
parser.context().value_names.push((content_variable, type_of_bytecode));
let body = hairy_expr(parser);
parser.context().value_names.pop();
parser.context().value_names.pop();
let body = body?;
match parser.get()? {
(HairyToken::BlockEnd(end_tag), info) => {
if !end_tag.is_empty() && "for" != end_tag.trim() {
let err_msg = write!(parser.context().allocator, "start (for) and end ({}) tag should be the same", end_tag.trim());
return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
}
retval.push(HairyCommand::Loop(expr_bytecode, index_variable, content_variable, body, source_context));
Ok(true)
}
(token, info) => Err(parser.error_token(token, info, |_| {
HairyParserError::Parser("expected 'end' of loop")
})),
}
},
(HairyToken::TemplateDef(name, args), info) => {
let mut compiled_args = Vec::new(); for (label, type_spec, offset) in args {
let type_spec = expry_parse_type(type_spec, parser.context().allocator).map_err(|CompileError{error,start,end, ..}| parser.error_other(&info.bound(start+offset, end+offset), HairyParserError::Expr(error)))?;
compiled_args.push((*label, type_spec));
}
core::mem::swap(&mut compiled_args, &mut parser.context().value_names);
let body = hairy_expr(parser);
core::mem::swap(&mut compiled_args, &mut parser.context().value_names);
let body = body?;
match parser.get()? {
(HairyToken::BlockEnd(end_tag), info) => {
if !end_tag.is_empty() && "define" != end_tag {
let err_msg = write!(parser.context().allocator, "start (define) and end ({}) tag should be the same", end_tag);
return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)))
}
let filename = parser.context().filename;
if parser.context().local_defines.iter().any(|x| x.name == name) {
let err_msg = write!(parser.context().allocator, "template '{}' already defined", name);
return Err(parser.error_other(&info, HairyParserError::Parser(err_msg)));
}
parser.context().local_defines.push(Define{name, filename, args: compiled_args.iter().map(|(_,x)| x.clone()).collect(), body, subtemplates: Vec::new()});
Ok(true)
}
(token, info) => Err(parser.error_token(token, info, |_| {
HairyParserError::Parser("expected 'end' of template definition")
})),
}
},
(HairyToken::Arguments(args), info) => {
let mut compiled_args = Vec::new(); for (label, type_spec, offset) in args {
let type_spec = expry_parse_type(type_spec, parser.context().allocator).map_err(|CompileError{error,start,end, ..}| parser.error_other(&info.bound(start+offset, end+offset), HairyParserError::Expr(error)))?;
compiled_args.push((*label, type_spec));
}
if let Some(count) = parser.context().value_count {
if count != args.len() {
let err = write!(parser.context().allocator, "different number of arguments in both code and template itself: {} and {}", count, args.len());
return Err(parser.error_other(&info, HairyParserError::Parser(err)));
}
let mut value_names = core::mem::take(&mut parser.context().value_names);
for (i,(k, v)) in value_names.iter_mut().enumerate() {
if let Some((specified_name,specified)) = compiled_args.get(i) {
if !k.is_empty() && *k != "_" && *specified_name != "_" && k != specified_name {
let err = write!(parser.context().allocator, "argument {} are named both in code and in the template with different names: {} and {}", i, *k, *specified_name);
return Err(parser.error_other(&info, HairyParserError::Parser(err)));
}
*k = specified_name;
if v.used_as(specified) {
if !matches!(specified, ExpryType::Any) {
*v = specified.clone();
}
} else {
let err = write!(parser.context().allocator, "argument {} named '{}' have conflicting types in code and in the template itself: {} can not be used as {}", i, k, v, specified);
return Err(parser.error_other(&info, HairyParserError::Parser(err)));
}
} else {
return Err(parser.error_other(&info, HairyParserError::Parser("expected a different number of arguments")));
}
}
parser.context().value_names = value_names;
} else if parser.context().value_names.is_empty() {
parser.context().value_names.extend_from_slice(&compiled_args);
} else {
return Err(parser.error_other(&info, HairyParserError::Parser("setting 'arguments' can only happen outside defines")));
}
Ok(true)
},
(HairyToken::Inline(name, expr, offset_expr), info) => {
let context = parser.context();
let (bytecode, _dynamic, _return_type,warnings) = expry_compile_expr_typed(expr, Some(&context.inlines), None, &[], context.custom_types, context.allocator).map_err(|e| parser.error_other(&info.bound(e.error_start()+offset_expr, e.error_end()+offset_expr), HairyParserError::Expr(e.error())))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_parser_error(&warnings, parser.context().allocator);
return Err(parser.error_other(&info, hairy_error));
}
let context = parser.context();
let value = expry_eval_func(bytecode, &mut Vec::new(), context.allocator, context.static_custom).map_err(|err| parser.error_other(&info, HairyParserError::Eval(err)))?;
parser.context().inlines.insert(key_str(name), value);
Ok(true)
},
(token, info) => Err(parser.error_token(token, info, |_| HairyParserError::Parser(""))),
}
})?;
Ok(retval)
}
fn expry_type_warnings_to_hairy_eval_error<'b,'c>(warnings: &[ExpryTypeWarning], allocator: &mut MemoryScope<'c>) -> HairyEvalError<'b> where 'c: 'b {
let mut buffer = ScopedStringBuilder::new(allocator);
write!(&mut buffer, "possibly non existant field used without error handling: ").unwrap();
for w in warnings {
match w {
ExpryTypeWarning::PossibleUnsetField(name) => { write!(&mut buffer, "{}, ", String::from_utf8_lossy(name)).unwrap(); },
}
}
HairyEvalError::Error(buffer.build())
}
fn expry_type_warnings_to_hairy_parser_error<'b,'c>(warnings: &[ExpryTypeWarning], allocator: &mut MemoryScope<'c>) -> HairyParserError<'b> where 'c: 'b {
let mut buffer = ScopedStringBuilder::new(allocator);
write!(&mut buffer, "possibly non existant field used without error handling: ").unwrap();
for w in warnings {
match w {
ExpryTypeWarning::PossibleUnsetField(name) => { write!(&mut buffer, "{}, ", String::from_utf8_lossy(name)).unwrap(); },
}
}
HairyParserError::Type(buffer.build())
}
fn hairy_top_level<'b,'c,'e>(parser: &'_ mut HairyParserState<'_,'b,'c,'e>) -> HairyParserResult<'b,AST<'b>> where 'c: 'b, 'e: 'b {
let retval = hairy_expr(parser)?;
parser.accept(HairyToken::EndOfInput(), None, || HairyParserError::Parser("expected end of input"))?;
Ok(retval)
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum EscapeMode {
Normal,
InsideTag,
InsideAttributeSingleQuote,
InsideAttributeDoubleQuote,
InsideScript,
InsideStyle,
InsideComment,
}
fn mode_to_bytes(mode: EscapeMode, tag: &str, attribute: &str) -> &'static [u8] {
match mode {
EscapeMode::Normal => b"", EscapeMode::InsideTag => b"",
EscapeMode::InsideAttributeDoubleQuote | EscapeMode::InsideAttributeSingleQuote if is_url(tag, attribute) => b"url",
EscapeMode::InsideAttributeDoubleQuote | EscapeMode::InsideAttributeSingleQuote => b"+",
EscapeMode::InsideScript => b"sjs", EscapeMode::InsideStyle => b"",
EscapeMode::InsideComment => b"",
}
}
fn is_url(tag: &str, attribute: &str) -> bool {
if attribute == "srcset" && tag == "source" {
return true;
}
if attribute == "object" && tag == "data" {
return true;
}
if attribute == "action" && tag == "form" {
return true;
}
if attribute == "href" && (tag == "a" || tag == "area" || tag == "base" || tag == "link") {
true
} else {
attribute == "src" && (tag == "audio" || tag == "embed" || tag == "iframe" || tag == "img" || tag == "input" || tag == "script" || tag == "source" || tag == "track" || tag == "video")
}
}
fn parse_html<'a,'b, 'c>(input: &'b str, extra_line_no: u32, scope: &mut MemoryScope<'c>) -> Result<&'a [(usize,&'static [u8])], HairyCompileError<'b>> where 'c: 'a {
let mut retval = ScopedArrayBuilder::new(scope);
let mut inside = 0;
let mut inside_string = false;
let mut mode = EscapeMode::Normal;
let mut tag = "";
let mut attribute = ""; let mut last_mode = mode;
let mut last_mode_bytes = mode_to_bytes(mode, tag, attribute);
retval.push((0, mode_to_bytes(mode, tag, attribute)));
for (i,c) in input.char_indices() {
if inside_string {
inside_string = c != '"';
continue;
}
match c {
'{' => inside += 1,
'}' => inside -= 1,
'"' if inside > 0 => inside_string = true,
_ => {},
}
if inside > 0 {
continue;
}
match mode {
EscapeMode::Normal => {
if c == '<' {
if input[i..].starts_with("<!--") {
mode = EscapeMode::InsideComment;
tag = "!--";
} else if let Some((prefix, _)) = input[i+1..].split_once(|x: char| !x.is_ascii_alphanumeric() && x != '/') {
if !prefix.is_empty() {
mode = EscapeMode::InsideTag;
tag = prefix;
}
}
attribute = "";
}
},
EscapeMode::InsideTag => {
match c {
'=' => {
if !input[i..].starts_with("=\"") && !input[i..].starts_with("=\'") {
return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("for auto escaping to work, attributes of HTML should have quotes around the values"), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no});
}
}
'"' => {
mode = EscapeMode::InsideAttributeDoubleQuote;
},
'\'' => {
mode = EscapeMode::InsideAttributeSingleQuote;
},
'>' => {
mode = if tag == "script" { EscapeMode::InsideScript } else if tag == "style" { EscapeMode::InsideStyle } else { EscapeMode::Normal };
},
'<' => {
return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("unexpected character encounted inside tag, likely to break auto escaping."), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
}
c if attribute.is_empty() && c.is_ascii_alphanumeric() => {
if let Some((prefix,_)) = input[i..].split_once(|x: char| !x.is_ascii_alphanumeric() && x != '_') {
attribute = prefix;
}
},
' ' => {
attribute = "";
},
_ => {
},
};
},
EscapeMode::InsideAttributeDoubleQuote => {
if c == '"' {
mode = EscapeMode::InsideTag;
attribute = "";
} else if c == '<' || c == '>' {
return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("unexpected character encounted inside attribute, likely to break auto escaping."), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
}
},
EscapeMode::InsideAttributeSingleQuote => {
if c == '\'' {
mode = EscapeMode::InsideTag;
attribute = "";
} else if c == '<' || c == '>' {
return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("unexpected character encounted inside attribute, likely to break auto escaping."), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
}
},
EscapeMode::InsideScript => {
if c == '<' {
if input[i..].starts_with("</script>") {
mode = EscapeMode::InsideTag;
tag = "/script";
} else {
}
}
},
EscapeMode::InsideStyle => {
if c == '<' {
if input[i..].starts_with("</style>") {
mode = EscapeMode::InsideTag;
tag = "/style";
} else {
return Err(HairyCompileError{expr: input, error: HairyParserError::Parser("expected end of style tag"), start: input.len()-i, end: input.len()-i, extra: None, line_context: None, extra_line_no, });
}
}
},
EscapeMode::InsideComment => {
if input[i..].starts_with("-->") {
mode = EscapeMode::Normal;
}
}
}
if mode != last_mode {
if mode_to_bytes(mode, tag, attribute) != last_mode_bytes {
last_mode_bytes = mode_to_bytes(mode, tag, attribute);
retval.push((i+1, last_mode_bytes));
}
last_mode = mode;
}
}
if mode != EscapeMode::Normal {
let message = match mode {
EscapeMode::Normal => "",
EscapeMode::InsideTag => "expected to end with correctly closed tag, now 'inside tag' mode",
EscapeMode::InsideAttributeDoubleQuote => "expected to end with correctly closed tag, now 'inside attribute with double quotes' mode",
EscapeMode::InsideAttributeSingleQuote => "expected to end with correctly closed tag, now 'inside attribute with single quotes' mode",
EscapeMode::InsideScript => "expected to end with correctly closed tag, now 'inside script' mode",
EscapeMode::InsideStyle => "expected to end with correctly closed tag, now 'inside style' mode",
EscapeMode::InsideComment => "expected to end with correctly closed HTML comment, now 'inside HTML comment' mode",
};
return Err(HairyCompileError{expr: input, error: HairyParserError::Parser(message), start: 0, end: 0, extra: None, line_context: None, extra_line_no, });
}
Ok(retval.build())
}
fn lookup_escape_mode<'a>(escaping: &'_ [(usize, &'a [u8])], pos: usize) -> &'a [u8] {
let pp = escaping.partition_point(|x| x.0 <= pos);
if pp > 0 {
return escaping[pp-1].1;
}
b""
}
#[derive(Clone)]
pub struct HairyOptions<'b,'c,'d> {
pub custom: &'c dyn CustomFuncs<'d>,
dynamic_values: Vec<(&'b str, EncodedValueRef<'b>)>,
pub escaper: &'c dyn Escaper,
pub given_templates: Option<&'c [RawReader<'b>]>,
pub inlines: DecodedObject<'b>,
pub extra_line_no: u32,
pub strip_spaces: bool,
}
impl<'b,'c,'d> HairyOptions<'b,'c,'d> {
pub fn new() -> Self {
Self {
custom: &NoCustomFuncs{},
dynamic_values: Vec::new(),
escaper: &DefaultEscaper{ default_escape_mode: b"html" },
given_templates: None,
inlines: DecodedObject::new(),
extra_line_no: 0,
strip_spaces: false,
}
}
pub fn custom<T: CustomFuncs<'d>>(&mut self, custom: &'c T) {
self.custom = custom;
}
pub fn set_named_dynamic_values(&mut self, args: &[(&'b str, EncodedValueRef<'b>)]) {
self.dynamic_values.clear();
self.dynamic_values.extend_from_slice(args);
}
pub fn set_dynamic_values(&mut self, args: &[EncodedValueRef<'b>]) {
self.dynamic_values.clear();
let placeholder : &'b str = "";
self.dynamic_values.extend(args.iter().map(|x| (placeholder, *x)));
}
}
impl<'b,'c,'d> Default for HairyOptions<'b,'c,'d> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct HairyCompileOptions<'b,'c,'d> {
pub custom_types: std::collections::BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)>,
pub dynamic_values_types: Vec<(&'b str, ExpryType)>,
pub dynamic_values_count: Option<usize>,
pub static_custom: &'c dyn CustomFuncs<'d>,
pub inlines: DecodedObject<'b>,
pub defines: Option<std::collections::BTreeMap<Vec<u8>,Vec<ExpryType>>>,
pub override_name: Option<&'b str>,
pub extra_line_no: u32,
pub strip_spaces: bool,
}
impl<'b,'c,'d> HairyCompileOptions<'b,'c,'d> {
pub fn new() -> Self {
Self {
custom_types: std::collections::BTreeMap::new(),
static_custom: &NoCustomFuncs{},
inlines: DecodedObject::new(),
defines: None,
override_name: None,
dynamic_values_types: Vec::new(),
dynamic_values_count: None,
extra_line_no: 0,
strip_spaces: false,
}
}
pub fn static_custom<T: CustomFuncs<'d>>(&mut self, custom: &'c T) {
self.static_custom = custom;
}
pub fn set_dynamic_value_count(&mut self, args: usize) {
self.dynamic_values_types.resize(args, ("", ExpryType::Any));
self.dynamic_values_count = Some(args);
}
pub fn set_dynamic_value_name_and_types(&mut self, args: &[(&'b str, ExpryType)]) {
self.dynamic_values_types.clear();
self.dynamic_values_types.extend_from_slice(args);
self.dynamic_values_count = Some(args.len());
}
pub fn set_dynamic_value_types(&mut self, args: &[ExpryType]) {
self.dynamic_values_types.clear();
self.dynamic_values_types.extend(args.iter().map(|x| ("", x.clone())));
self.dynamic_values_count = Some(args.len());
}
pub fn set_dynamic_value_names(&mut self, args: &[&'b str]) {
self.dynamic_values_types.clear();
self.dynamic_values_types.extend(args.iter().map(|x| (*x, ExpryType::Any)));
self.dynamic_values_count = Some(args.len());
}
}
impl<'b,'c,'d> Default for HairyCompileOptions<'b,'c,'d> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct HairyEvalOptions<'b,'c,'d> {
pub custom: &'c dyn CustomFuncs<'d>,
pub escaper: &'c dyn Escaper,
pub values: Vec<EncodedValueRef<'b>>,
pub given_templates: Option<&'c [RawReader<'b>]>, }
impl<'b,'c,'d> HairyEvalOptions<'b,'c,'d> {
pub fn new() -> Self {
Self {
custom: &NoCustomFuncs{},
escaper: &DefaultEscaper{ default_escape_mode: b"html" },
values: Vec::new(),
given_templates: None,
}
}
pub fn custom<T: CustomFuncs<'d>>(&mut self, custom: &'c T) {
self.custom = custom;
}
}
impl<'b,'c,'d> Default for HairyEvalOptions<'b,'c,'d> {
fn default() -> Self {
Self::new()
}
}
impl<'b,'c,'d> TryFrom<&HairyOptions<'b,'c,'d>> for HairyCompileOptions<'b,'c,'d> {
type Error = EncodingError;
fn try_from(v: &HairyOptions<'b,'c,'d>) -> Result<Self, Self::Error> {
Ok(Self {
custom_types: v.custom.types(),
dynamic_values_types: v.dynamic_values.iter().map(|(name,value)| -> Result<_,_> { Ok((*name, expry_to_type_lazy(&expry_decode_lazy(value)?)?))}).try_fold(Vec::new(), |mut acc,v| -> Result<Vec<_>,EncodingError> { acc.push(v?); Ok(acc) })?,
dynamic_values_count: Some(v.dynamic_values.len()),
static_custom: v.custom,
inlines: v.inlines.clone(),
defines: v.given_templates.as_ref().map(|x| hairy_templates_to_types(x)).map_or(Ok(None), |v| v.map(Some))?,
override_name: None,
extra_line_no: v.extra_line_no,
strip_spaces: v.strip_spaces,
})
}
}
impl<'b,'c,'d> TryFrom<&HairyOptions<'b,'c,'d>> for HairyEvalOptions<'b,'c,'d> {
type Error = EncodingError;
fn try_from(v: &HairyOptions<'b,'c,'d>) -> Result<Self, Self::Error> {
Ok(Self {
custom: v.custom,
escaper: v.escaper,
values: v.dynamic_values.iter().map(|(_,v)| *v).collect(),
given_templates: v.given_templates,
})
}
}
pub fn hairy_compile<'a,'b,'c,'d,'e>(reader: &'b str, filename: &'b str, options: &'a HairyCompileOptions<'b,'d,'e>, scope: &mut MemoryScope<'c>, escaping: Option<&'_ [(usize, &'b [u8])]>) -> Result<BytecodeVec,HairyCompileError<'b>> where 'c: 'b, 'b: 'a, 'e: 'b {
if reader.len() >= u32::MAX as usize {
return Err(HairyCompileError{expr: reader, error: HairyParserError::Other("input too large to parse (max 4 GiB)"), start: reader.len(), end: 0usize, extra: None, line_context: None, extra_line_no: options.extra_line_no});
}
let context = ParserContext {
allocator: scope,
line_context: LineContext::new(reader),
local_defines: Vec::new(),
outside_defines: options.defines.as_ref(),
all_defines_known: options.defines.is_some(),
inlines: options.inlines.clone(),
value_names: options.dynamic_values_types.iter().map(|(name,t)| (*name, t.clone())).collect(),
value_count: options.dynamic_values_count,
filename,
custom_types: &options.custom_types,
static_custom: options.static_custom,
escaping,
input_len: reader.len(),
extra_line_no: options.extra_line_no,
trim_spaces: options.strip_spaces,
};
let mut parser = HairyParserState::new_with(reader, hairy_tokenize, context);
let ast = match parser.parse(hairy_top_level, HairyParserError::Parser("unexpected token"), HairyParserError::Parser("max recursion depth of parser reached")) {
Ok(ast) => ast,
Err((e,start,end,extra)) => return Err(HairyCompileError{expr: reader, error: e, start, end, extra, line_context: Some(parser.context.line_context), extra_line_no: options.extra_line_no}),
};
debug_assert!(parser.context().value_count.is_none() || parser.context().value_names.len() == options.dynamic_values_types.len()); let ast = ast.into_iter().fold(Vec::new(), |mut acc, item| {
if let Some(last) = acc.last_mut() {
if let (HairyCommand::Text(a), HairyCommand::Text(b)) = (last, &item) {
*a = parser.context().allocator.concat_u8(&[a, b]);
return acc;
}
}
acc.push(item);
acc
});
let context = parser.consume();
(|| {
let name : &str = options.override_name.unwrap_or(filename);
let define = Define{name, filename, args: context.value_names.iter().map(|(_,y)| y.clone()).collect(), body: ast, subtemplates: context.local_defines };
let mut length_collector = RawWriterLength::new();
template_to_binary(&define, &mut length_collector).unwrap_infallible();
let total_length = length_collector.length();
let mut bytecode: Vec<u8> = vec![0; total_length];
let mut writer = RawWriter::with(&mut bytecode);
template_to_binary(&define, &mut writer)?;
debug_assert_eq!(0, writer.left());
Ok(BytecodeVec(bytecode))
})().map_err(|x| HairyCompileError{expr: reader, error: x, start:0, end:0, extra: None, line_context: Some(context.line_context), extra_line_no: options.extra_line_no})
}
pub fn hairy_extract_subtemplates(template_bytecode: BytecodeRef) -> Result<&[u8],HairyError> {
let define = decode_hairy(template_bytecode.get())?;
Ok(define.subtemplates.get())
}
#[derive(Clone)]
struct CheckTypeContext<'a,'b> {
custom_types: &'a std::collections::BTreeMap<Key<'a>, (Vec<ExpryType>, ExpryType)>,
templates: Vec<RawReader<'b>>,
template_name: &'b [u8],
values: Vec<&'b ExpryType>,
escaper: Option<&'a dyn Escaper>,
}
fn check_types<'a,'b,'c,'e>(expression: &mut RawReader<'b>, context: &'_ mut CheckTypeContext<'a,'b>, allocator: &'_ mut MemoryScope<'c>, depth: usize) -> Result<(),HairyError<'e>> where 'b: 'a, 'c: 'e {
if depth == 0 {
return Err(HairyEvalError::EvalTooLong().into());
}
'next: while !expression.is_empty() {
let opcode = expression.read_u8().map_err(|_| HairyEvalError::Bytecode("Expected more bytes in expression"))?;
if opcode == HairyBytecode::Text as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted TEXT");
let text_length = expression.read_var_u64().map_err(err)?;
let _text = expression.read_bytes(text_length as usize).map_err(err)?;
continue;
}
if opcode == HairyBytecode::Evaluate as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted EXPR");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let escape_mode = expression.read_var_string().map_err(err)?;
let (value_type, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
return Err(HairyError::wrap(hairy_error, HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no));
}
if let Some(escaper) = context.escaper {
if !escaper.check_type(&value_type, escape_mode) {
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "unsupported type by specified escaper: {} at {}:{}", value_type, line_no, column_no)), HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no));
}
}
continue;
}
if opcode == HairyBytecode::ConditionalBool as u8 || opcode == HairyBytecode::ConditionalLet as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted CONDITIONAL");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let body = expression.read_var_string().map_err(err)?;
let else_body = expression.read_var_string().map_err(err)?;
if opcode == HairyBytecode::ConditionalLet as u8 {
let return_type = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator);
let (return_type, warnings) = return_type.map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Eval, allocator.copy_u8(context.template_name), line_no, column_no))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
return Err(HairyError::wrap(hairy_error, HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
}
let return_type = match return_type {
ExpryType::Nullable(x) => *x,
x => x,
};
if !else_body.is_empty() {
check_types(&mut RawReader::with(else_body), context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
}
if !matches!(return_type, ExpryType::Null) {
let mut context = context.clone();
context.values.push(&return_type);
let retval = check_types(&mut RawReader::with(body), &mut context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
context.values.pop();
retval?;
}
continue;
}
let (retval, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{}", err)),HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
return Err(HairyError::wrap(hairy_error, HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
}
'handle: {
if matches!(retval, ExpryType::Any | ExpryType::Bool | ExpryType::Null | ExpryType::Nullable(_)) {
if let ExpryType::Nullable(inner) = &retval {
if !matches!(**inner, ExpryType::Bool) {
break 'handle;
}
}
check_types(&mut RawReader::with(body), context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
if !else_body.is_empty() {
check_types(&mut RawReader::with(else_body), context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no))?;
}
continue 'next;
}
}
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in conditional should result in a bool or null, not {retval}")), HairyLocationType::Conditional, allocator.copy_u8(context.template_name), line_no, column_no));
}
if opcode == HairyBytecode::Loop as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted LOOP");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let body = expression.read_var_string().map_err(err)?;
let (mut return_type, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
return Err(HairyError::wrap(hairy_error, HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no));
}
if let ExpryType::Nullable(iteration_type) = return_type {
return_type = *iteration_type;
}
if let ExpryType::Array(iteration_type) = return_type {
let mut context = context.clone();
context.values.push(&ExpryType::Int);
context.values.push(&iteration_type);
let retval = check_types(&mut RawReader::with(body), &mut context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no));
retval?;
continue;
}
if let ExpryType::Any = return_type {
let mut context = context.clone();
context.values.push(&ExpryType::Int);
context.values.push(&ExpryType::Any);
let retval = check_types(&mut RawReader::with(body), &mut context, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no));
retval?;
continue;
}
if matches!(return_type, ExpryType::Null | ExpryType::EmptyArray) {
continue;
}
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in loop should result in an array (with any value) or null, not {return_type}")), HairyLocationType::Loop, allocator.copy_u8(context.template_name), line_no, column_no));
}
if opcode == HairyBytecode::CallDynamicName as u8 || opcode == HairyBytecode::CallDynamicBody as u8 || opcode == HairyBytecode::CallStatic as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted CALL");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let name = expression.read_var_string().map_err(err)?;
let args_count = expression.read_var_u64().map_err(err)?;
let templates = context.templates.clone();
let result : CompiledDefine;
if opcode == HairyBytecode::CallDynamicName as u8 || opcode == HairyBytecode::CallDynamicBody as u8 {
return Ok(());
} else if opcode == HairyBytecode::CallStatic as u8 {
result = match resolve_template(name, &context.templates).map_err(|_| HairyEvalError::Error(write!(allocator, "template definitions problems: '{}' not found", String::from_utf8_lossy(name))))? {
None => {
return Err(HairyError::wrap(HairyEvalError::TemplateNotFound(allocator.copy_u8(name)),HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
},
Some(values) => values,
};
} else {
return Err(HairyError::wrap(HairyEvalError::Error("unrecognized loop opcode in template bytecode"),HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
}
if result.args_count != args_count || args_count > MAX_TEMPLATE_ARGS {
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "call of template '{}' with {} arguments instead of expected {} arguments", String::from_utf8_lossy(result.name), args_count, result.args_count)), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
}
let mut values = Vec::new();
let mut arg_type_reader = RawReader::with(result.lazy_arg_types);
let mut changed = false;
for i in 0..args_count {
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let (actual_arg_type, warnings) = expry_type_from_bytecode(expr, &context.values, context.custom_types, allocator).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no))?;
if !warnings.is_empty() {
let hairy_error = expry_type_warnings_to_hairy_eval_error(&warnings, allocator);
return Err(HairyError::wrap(hairy_error, HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
}
let mut arg_type = expry_parse_type(&String::from_utf8_lossy(arg_type_reader.read_var_string()?), allocator).map_err(|_| HairyError::wrap(HairyEvalError::Error(write!(allocator, "error in bytecode of arguments of define '{}' in file '{}'", String::from_utf8_lossy(result.name), String::from_utf8_lossy(result.filename))), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no))?;
if let (true, specialized) = actual_arg_type.specialize(&arg_type) {
if let Some(specialized) = specialized {
arg_type = specialized;
changed = true;
}
} else {
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "call of template '{}' with wrong type for argument {}: expected {}, but {} can not be used as that", String::from_utf8_lossy(result.name), i, arg_type, actual_arg_type)), HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no));
}
values.push(arg_type);
}
if context.template_name != result.name && changed {
let mut context2 = CheckTypeContext { templates, template_name: result.name, custom_types: context.custom_types, values: values.iter().collect(), escaper: context.escaper };
check_types(&mut RawReader::with(result.body.get()), &mut context2, allocator, depth-1).map_err(|err| err.add_location(HairyLocationType::Call, allocator.copy_u8(context.template_name), line_no, column_no))?;
}
continue;
}
return Err(HairyEvalError::Bytecode(write!(allocator, "unrecognized opcode {opcode}")).into());
}
Ok(())
}
pub fn hairy_check_types<'a,'b,'c>(template_bytecode: BytecodeRef<'b>, dynamic_value_types: Vec<&ExpryType>, given_templates: Option<&'a [RawReader<'b>]>, custom_types: &'a std::collections::BTreeMap<Key<'a>, (Vec<ExpryType>, ExpryType)>, escaper: Option<&'a dyn Escaper>, allocator: &mut MemoryScope<'c>) -> Result<(),HairyError<'b>> where 'c: 'b, 'b: 'a {
let CompiledDefine{name, body, subtemplates, args_count, ..} = decode_hairy(template_bytecode.get())?;
if dynamic_value_types.len() != args_count as usize {
return Err(HairyEvalError::Error(write!(allocator, "template called with wrong number of arguments (values), {} instead of {}", dynamic_value_types.len(), args_count)).into());
}
let mut templates = Vec::with_capacity(given_templates.as_ref().map(|x| x.len()).unwrap_or_default() + 1);
if let Some(given_templates) = &given_templates {
templates.extend_from_slice(given_templates);
}
templates.push(RawReader::with(&subtemplates));
let mut context = CheckTypeContext { templates, template_name: name, custom_types, values: dynamic_value_types, escaper };
let mut bytecode_reader = RawReader::with(body.get());
check_types(&mut bytecode_reader, &mut context, allocator, MAX_TEMPLATE_RECURSION)?;
Ok(())
}
pub fn hairy_compile_error_format_console(e: &mut HairyCompileError) -> (u32, String) {
let mut retval = String::new();
let (expr, start, end, extra_line_no, error, extra) = (e.expr, e.start, e.end, e.extra_line_no, e.error, e.extra);
let line_context = e.get_line_context();
let (line_no, prefix, error_msg) = line_context.format_error_context_console(expr, start, end, extra_line_no).map_or((0u32, String::new(), String::new()), |x| x);
if line_no > 0 {
write!(retval, "{}{}error at line {}:{} {}\n{}", prefix, TERM_BRIGHT_RED, line_no, TERM_RESET, error, error_msg).ok();
} else {
write!(retval, "unknown error, error during hairy_compile_error_format").ok();
}
if let Some((start, end)) = extra {
let (line_no, prefix, error_msg) = line_context.format_error_context_console(expr, start, end, extra_line_no).map_or((0u32, String::new(), String::new()), |x| x);
if line_no > 0 {
write!(retval, "{}{}related at line {}:{} expected it here (or earlier)\n{}", prefix, TERM_DIM_CYAN, line_no, TERM_RESET, error_msg).ok();
}
}
(line_no, retval)
}
pub fn hairy_compile_error_format_html(e: &mut HairyCompileError) -> (u32, String) {
let mut retval = String::new();
let (expr, start, end, extra_line_no, error, extra) = (e.expr, e.start, e.end, e.extra_line_no, e.error, e.extra);
let line_context = e.get_line_context();
let (line_no, error_msg) = line_context.format_error_context_html(expr, start, end, extra_line_no).map_or((0u32, String::new()), |x| x);
if line_no > 0 {
write!(retval, "error at line {}: {}\n{}", line_no, error, error_msg).ok();
} else {
write!(retval, "unknown error, error during hairy_compile_error_format").ok();
}
if let Some((start, end)) = extra {
let (line_no, error_msg) = line_context.format_error_context_html(expr, start, end, extra_line_no).map_or((0u32, String::new()), |x| x);
if line_no > 0 {
write!(retval, "related at line {}: expected it here (or earlier)\n{}", line_no, error_msg).ok();
}
}
(line_no, retval)
}
pub fn hairy_compile_html<'b>(reader: &'b str, filename: &'b str, options: &HairyCompileOptions) -> Result<BytecodeVec,String> {
let mut allocator = MemoryPool::new();
let mut scope = allocator.rewind();
hairy_compile_html_scope(reader, filename, options, &mut scope).map_err(|mut err| hairy_compile_error_format_console(&mut err).1)
}
pub fn hairy_compile_html_scope<'b,'c,'e>(reader: &'b str, filename: &'b str, options: &HairyCompileOptions<'b,'_,'e>, scope: &mut MemoryScope<'c>) -> Result<BytecodeVec,HairyCompileError<'b>> where 'c: 'b, 'e: 'b {
let escaping = parse_html(reader, options.extra_line_no, scope)?;
hairy_compile(reader, filename, options, scope, Some(escaping))
}
fn template_to_binary<E, Out: RawOutput<E>>(template: &Define, writer: &mut Out) -> Result<(), E> {
let Define{name, filename, args, body, subtemplates} = template;
writer.write_bytes(HAIRY_MAGIC)?;
writer.write_var_bytes(name.as_bytes())?;
writer.write_var_bytes(filename.as_bytes())?;
writer.write_var_u64(args.len() as u64)?;
write_with_header(writer, Out::write_var_u64, |writer| {
for type_spec in args {
let type_spec = format!("{type_spec}");
writer.write_var_bytes(type_spec.as_bytes())?;
}
Ok(())
})?;
write_with_header(writer, Out::write_var_u64, |writer| {
ast_to_binary(body, writer)
})?;
write_with_header(writer, Out::write_var_u64, |writer| {
templates_to_binary(subtemplates, writer)
})
}
fn templates_to_binary<E, Out: RawOutput<E>>(templates: &[Define], writer: &mut Out) -> Result<(), E> {
for d in templates {
template_to_binary(d, writer)?;
}
Ok(())
}
fn templates_to_binary_size(templates: &[Define]) -> usize {
let mut length_collector = RawWriterLength::new();
templates_to_binary(templates, &mut length_collector).unwrap_infallible();
length_collector.length()
}
fn ast_to_binary_size(ast: &[HairyCommand]) -> usize {
let mut length_collector = RawWriterLength::new();
ast_to_binary(ast, &mut length_collector).unwrap_infallible();
length_collector.length()
}
enum HairyBytecode {
Return = 0,
Text = 1,
Evaluate = 2,
Loop = 4,
ConditionalBool = 5,
ConditionalLet = 6,
CallStatic = 7,
CallDynamicName = 8,
CallDynamicBody = 9,
}
fn ast_to_binary<E, Out: RawOutput<E>>(ast: &[HairyCommand], writer: &mut Out) -> Result<(), E> {
for cmd in ast {
match cmd {
HairyCommand::Text(text) => {
writer.write_u8(HairyBytecode::Text as u8)?;
writer.write_var_bytes(text)?;
},
HairyCommand::Conditional(conditions, else_body) => {
generate_conditions(conditions, else_body, writer)?;
},
HairyCommand::Loop(expr, _, _, body, source_context) => {
writer.write_u8(HairyBytecode::Loop as u8)?;
writer.write_var_u64(source_context.0 as u64)?;
writer.write_var_u64(source_context.1 as u64)?;
writer.write_var_bytes(expr.get())?;
write_with_header(writer, Out::write_var_u64, |writer| ast_to_binary(body, writer))?;
},
HairyCommand::Eval(expr, escape_mode, source_context) => {
writer.write_u8(HairyBytecode::Evaluate as u8)?;
writer.write_var_u64(source_context.0 as u64)?;
writer.write_var_u64(source_context.1 as u64)?;
writer.write_var_bytes(expr.get())?;
writer.write_var_bytes(escape_mode)?;
},
HairyCommand::CallTemplate(name, args, source_context) => {
match name {
ResolveTemplate::Static(_) => writer.write_u8(HairyBytecode::CallStatic as u8)?,
ResolveTemplate::DynamicName(_) => writer.write_u8(HairyBytecode::CallDynamicName as u8)?,
ResolveTemplate::DynamicBody(_) => writer.write_u8(HairyBytecode::CallDynamicBody as u8)?,
}
writer.write_var_u64(source_context.0 as u64)?;
writer.write_var_u64(source_context.1 as u64)?;
match name {
ResolveTemplate::Static(name) => writer.write_var_bytes(name.as_bytes())?,
ResolveTemplate::DynamicName(expr) => writer.write_var_bytes(expr.get())?,
ResolveTemplate::DynamicBody(expr) => writer.write_var_bytes(expr.get())?,
}
writer.write_var_u64(args.len() as u64)?;
for (e,_return_type) in args {
writer.write_var_bytes(e.get())?;
}
},
}
}
Ok(())
}
fn generate_conditions<E, Out: RawOutput<E>>(conditions: &[(Option<Name>, BytecodeRef, Vec<HairyCommand>, SourceContext)], else_body: &[HairyCommand], writer: &mut Out) -> Result<(), E> {
if conditions.is_empty() {
ast_to_binary(else_body, writer)
} else {
let (variable, expr, body, source_context) = &conditions[0];
if variable.is_some() {
writer.write_u8(HairyBytecode::ConditionalLet as u8)?;
} else {
writer.write_u8(HairyBytecode::ConditionalBool as u8)?;
}
writer.write_var_u64(source_context.0 as u64)?;
writer.write_var_u64(source_context.1 as u64)?;
writer.write_var_bytes(expr.get())?;
write_with_header(writer, Out::write_var_u64,
|writer| ast_to_binary(body, writer))?;
write_with_header(writer, Out::write_var_u64,
|writer| generate_conditions(&conditions[1..], else_body, writer))
}
}
#[derive(Debug)]
pub enum HairyEvalError<'a> {
Bytecode(&'a str),
Evaluate(&'a str),
Error(&'a str),
EvalTooLong(), TemplateNotFound(&'a [u8]),
}
impl<'a> core::fmt::Display for HairyEvalError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HairyEvalError::Bytecode(msg) => write!(f, "{msg} (bytecode)"),
HairyEvalError::Evaluate(msg) => write!(f, "{msg} (eval)"),
HairyEvalError::Error(msg) => write!(f, "{msg} (error)"),
HairyEvalError::EvalTooLong() => write!(f, "eval too long"),
HairyEvalError::TemplateNotFound(msg) => write!(f, "template '{}' not found", String::from_utf8_lossy(msg)),
}
}
}
impl<'a> From<EncodingError> for HairyEvalError<'a> {
fn from(_: EncodingError) -> Self {
HairyEvalError::Error("hairy template bytecode error")
}
}
struct Context<'a,'b,'d> {
custom: &'a dyn CustomFuncs<'d>,
templates: &'a [RawReader<'b>],
template_name: &'b [u8],
escaper: &'a dyn Escaper,
values: Vec<EncodedValueRef<'b>>,
}
pub trait Escaper {
#[allow(clippy::ptr_arg)]
fn append_to_output<'b,'c,'d>(&self, value: DecodedValue<'b>, escape_mode: &'b [u8], allocator: &mut MemoryScope<'c>, output: &mut Vec<&'d [u8]>) -> Result<(),HairyEvalError<'d>> where 'c: 'd, 'b: 'd;
fn check_type(&self, value_type: &ExpryType, escape_mode: &[u8]) -> bool;
}
pub struct DefaultEscaper<'e> {
default_escape_mode: &'e [u8],
}
impl<'e> Default for DefaultEscaper<'e> {
fn default() -> Self { Self { default_escape_mode: b"html" } }
}
impl<'e> Escaper for DefaultEscaper<'e> {
fn append_to_output<'b,'c,'d>(&self, value: DecodedValue<'b>, escape_mode: &'b [u8], allocator: &mut MemoryScope<'c>, output: &mut Vec<&'d [u8]>) -> Result<(),HairyEvalError<'d>> where 'c: 'd, 'b: 'd {
let escape_mode = if escape_mode.is_empty() {
self.default_escape_mode
} else {
escape_mode
};
if escape_mode == b"sjs" || escape_mode == b"js" {
if !value.is_valid_json() {
return Err(HairyEvalError::Error("UTF-8 problem in js mode: maybe non-text is outputted in a key or a string?"));
}
let mut json = write!(allocator, "{value}").as_bytes();
if escape_mode == b"sjs" {
json = allocator.copy_with_replacement(json, html_escape_outside_attribute_u8);
}
output.push(json);
return Ok(());
}
match value {
DecodedValue::Null => Ok(()),
DecodedValue::Int(i) => {
output.push(write!(allocator, "{i}").as_bytes());
Ok(())
},
DecodedValue::Float(f) => {
let mut out = match escape_mode {
b"unrounded" => write!(allocator, "{f}"),
b"f0" => write!(allocator, "{f:.0}"),
b"f1" => write!(allocator, "{f:.1}"),
b"f2" => write!(allocator, "{f:.2}"),
b"f3" => write!(allocator, "{f:.3}"),
b"f4" => write!(allocator, "{f:.4}"),
b"f5" => write!(allocator, "{f:.5}"),
b"f6" => write!(allocator, "{f:.6}"),
_ => write!(allocator, "{f:.6}"),
};
if escape_mode == self.default_escape_mode {
out = &out[0..out.rfind(|x| x != '0').map_or(out.len(), |x| if out.as_bytes()[x] == b'.' { x+2 } else { x+1 })];
}
output.push(out.as_bytes());
Ok(())
},
DecodedValue::Double(f) => {
let mut out = match escape_mode {
b"unrounded" => write!(allocator, "{f}"),
b"f0" => write!(allocator, "{f:.0}"),
b"f1" => write!(allocator, "{f:.1}"),
b"f2" => write!(allocator, "{f:.2}"),
b"f3" => write!(allocator, "{f:.3}"),
b"f4" => write!(allocator, "{f:.4}"),
b"f5" => write!(allocator, "{f:.5}"),
b"f6" => write!(allocator, "{f:.6}"),
_ => write!(allocator, "{f:.14}"),
};
if escape_mode == self.default_escape_mode {
out = &out[0..out.rfind(|x| x != '0').map_or(out.len(), |x| if out.as_bytes()[x] == b'.' { x+2 } else { x+1 })];
}
output.push(out.as_bytes());
Ok(())
},
DecodedValue::String(s) => {
if escape_mode == b"html" {
output.push(allocator.copy_with_replacement(s, html_escape_outside_attribute_u8));
} else if escape_mode == b"+" {
output.push(allocator.copy_with_replacement(s, html_escape_inside_attribute_u8));
} else if escape_mode == b"url" {
output.push(allocator.copy_with_dynamic_replacement(s, url_escape_u8));
} else if escape_mode == b"none" {
output.push(s);
} else {
return Err(HairyEvalError::Evaluate(write!(allocator, "unsupported escape mode: {}", String::from_utf8_lossy(escape_mode))));
}
Ok(())
},
_ => Err(HairyEvalError::Evaluate("only numbers and strings can be outputted (in regular escape modes)")),
}
}
fn check_type(&self, mut value_type: &ExpryType, escape_mode: &[u8]) -> bool {
let escape_mode = if escape_mode.is_empty() {
self.default_escape_mode
} else {
escape_mode
};
if escape_mode == b"sjs" || escape_mode == b"js" {
return true;
}
if let ExpryType::Nullable(inner) = value_type {
value_type = &**inner;
}
match value_type {
ExpryType::Any => true, ExpryType::Null => true,
ExpryType::Int => true,
ExpryType::Float => true,
ExpryType::Double=> true,
ExpryType::String => true,
_ => false,
}
}
}
fn resolve_template<'b>(name: &'_ [u8], templates: &'_ [RawReader<'b>]) -> Result<Option<CompiledDefine<'b>>,HairyError<'b>> {
for reader in templates.iter().rev() {
let mut reader : RawReader = *reader;
while !reader.is_empty() {
let define = decode_hairy_reader(&mut reader)?;
if name == define.name {
return Ok(Some(define));
}
}
}
Ok(None)
}
pub fn hairy_templates_to_types(templates: &[RawReader<'_>]) -> Result<std::collections::BTreeMap<Vec<u8>, Vec<ExpryType>>,EncodingError> {
let mut allocator = MemoryPool::new();
let mut scope = allocator.rewind();
let mut retval = std::collections::BTreeMap::new();
for reader in templates.iter().rev() {
let mut reader : RawReader = *reader;
while !reader.is_empty() {
let define = decode_hairy(reader.read_var_string()?).map_err(|_| EncodingError{ line_nr: line!() })?;
if !retval.contains_key(define.name) {
let mut subreader = RawReader::with(define.lazy_arg_types);
let mut args = Vec::new();
for _ in 0..define.args_count {
let type_spec = subreader.read_var_string()?;
let type_spec = core::str::from_utf8(type_spec).map_err(|_| EncodingError{ line_nr: line!() })?;
let result = expry_parse_type(type_spec, &mut scope).map_err(|_| EncodingError{ line_nr: line!() })?;
args.push(result);
}
let _bytecode = BytecodeRef(subreader.read_var_string()?);
debug_assert_eq!(0, subreader.len());
retval.insert(define.name.to_vec(), args);
}
}
}
Ok(retval)
}
#[derive(Debug)]
pub enum HairyLocationType {
Conditional,
Loop,
Call,
Eval,
}
#[derive(Debug)]
pub struct HairyStackEntry<'a> {
location_type: HairyLocationType,
filename: &'a [u8],
line_no: u64,
column_no: u64,
}
pub struct HairyError<'a> {
error: HairyEvalError<'a>,
stack: Vec<HairyStackEntry<'a>>,
}
impl core::fmt::Display for HairyLocationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HairyLocationType::Conditional => write!(f, "cond"),
HairyLocationType::Loop => write!(f, "loop"),
HairyLocationType::Call => write!(f, "call"),
HairyLocationType::Eval => write!(f, "eval"),
}
}
}
impl<'a> core::fmt::Display for HairyError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Hairy error: {}", self.error)?;
for loc in &self.stack {
writeln!(f, " during {} in {} on line {}:{}", loc.location_type, String::from_utf8_lossy(loc.filename), loc.line_no, loc.column_no+1)?;
}
Ok(())
}
}
impl<'a> fmt::Debug for HairyError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl<'a> HairyError<'a> {
fn wrap(error: HairyEvalError<'a>, location_type: HairyLocationType, filename: &'a [u8], line_no: u64, column_no: u64) -> Self { Self { error, stack: vec![HairyStackEntry {
location_type,
filename,
line_no,
column_no,
}]} }
fn add_location(mut self, location: HairyLocationType, filename: &'a [u8], line_no: u64, column_no: u64) -> Self {
self.stack.push(HairyStackEntry{ location_type: location, filename, line_no, column_no });
self
}
}
impl<'a> From<EncodingError> for HairyError<'a> {
fn from(e: EncodingError) -> Self {
Self {
error: e.into(),
stack: Vec::new(),
}
}
}
impl<'a> From<HairyEvalError<'a>> for HairyError<'a> {
fn from(e: HairyEvalError<'a>) -> Self {
Self {
error: e,
stack: Vec::new(),
}
}
}
fn evaluate_to<'a,'b,'c,'d>(expression: &mut RawReader<'b>, context: &'_ mut Context<'a,'b,'d>, allocator: &mut MemoryScope<'c>, output: &mut Vec<&'b [u8]>, depth: usize) -> Result<(),HairyError<'b>> where 'c: 'b, 'b: 'a, 'd: 'b {
if depth == 0 {
return Err(HairyEvalError::EvalTooLong().into());
}
while !expression.is_empty() {
let opcode = expression.read_u8().map_err(|_| HairyEvalError::Bytecode("Expected more bytes in expression"))?;
if opcode == HairyBytecode::Text as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted TEXT");
let text_length = expression.read_var_u64().map_err(err)?;
let text = expression.read_bytes(text_length as usize).map_err(err)?;
output.push(text);
continue;
}
if opcode == HairyBytecode::Evaluate as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted EXPR");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let escape_mode = expression.read_var_string().map_err(err)?;
let object = expry_eval_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Eval, context.template_name, line_no, column_no))?;
context.escaper.append_to_output(object, escape_mode, allocator, output).map_err(|err| HairyError::wrap(err, HairyLocationType::Eval, context.template_name, line_no, column_no))?;
continue;
}
if opcode == HairyBytecode::ConditionalBool as u8 || opcode == HairyBytecode::ConditionalLet as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted CONDITIONAL");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let body = expression.read_var_string().map_err(err)?;
let else_body = expression.read_var_string().map_err(err)?;
if opcode == HairyBytecode::ConditionalLet as u8 {
let retval = expry_slice_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
if LazyDecodedValue::Null == expry_decode_lazy(&retval)? {
if !else_body.is_empty() {
evaluate_to(&mut RawReader::with(else_body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
}
continue;
}
context.values.push(retval);
let retval = evaluate_to(&mut RawReader::with(body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no));
context.values.pop();
retval?;
continue;
}
let retval = expry_eval_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
if let DecodedValue::Bool(true) = retval {
evaluate_to(&mut RawReader::with(body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
continue;
}
if matches!(retval, DecodedValue::Bool(_) | DecodedValue::Null) {
if !else_body.is_empty() {
evaluate_to(&mut RawReader::with(else_body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Conditional, context.template_name, line_no, column_no))?;
}
continue;
}
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in conditional should result in a bool or null, not {}", retval.type_string())), HairyLocationType::Conditional, context.template_name, line_no, column_no));
}
if opcode == HairyBytecode::Loop as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted LOOP");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let body = expression.read_var_string().map_err(err)?;
let retval = expry_slice_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")),HairyLocationType::Loop, context.template_name, line_no, column_no))?;
let parsed = expry_decode_lazy(retval.get())?;
if let LazyDecodedValue::Array(mut array) = parsed {
let mut index : i64 = 0;
while !array.is_empty() {
let iteration_value = array.get_raw()?;
let index_value = DecodedValue::Int(index).encode_to_scope(allocator);
context.values.push(index_value);
context.values.push(iteration_value);
let retval = evaluate_to(&mut RawReader::with(body), context, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Loop, context.template_name, line_no, column_no));
context.values.pop();
context.values.pop();
retval?;
index += 1;
}
continue;
}
if let LazyDecodedValue::Null = parsed {
continue;
}
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in loop should result in an array (with any value) or null, not {}", parsed.type_string())), HairyLocationType::Loop, context.template_name, line_no, column_no));
}
if opcode == HairyBytecode::CallDynamicName as u8 || opcode == HairyBytecode::CallDynamicBody as u8 || opcode == HairyBytecode::CallStatic as u8 {
let err = |_| HairyEvalError::Bytecode("corrupted LOOP");
let line_no = expression.read_var_u64().map_err(err)?;
let column_no = expression.read_var_u64().map_err(err)?;
let mut name = expression.read_var_string().map_err(err)?;
let args_count = expression.read_var_u64().map_err(err)?;
let result;
if opcode == HairyBytecode::CallDynamicName as u8 {
let name_value = expry_eval_func(BytecodeRef(name), &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, context.template_name, line_no, column_no))?;
if let DecodedValue::String(name_string) = name_value {
name = name_string;
} else if let DecodedValue::Null = name_value {
return Ok(());
} else {
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in dynamic name template call should resolve to a string (or null), not {}", name_value.type_string())), HairyLocationType::Call, context.template_name, line_no, column_no));
}
result = match resolve_template(name, context.templates).map_err(|_| HairyEvalError::Error(write!(allocator, "template definitions problems: '{}' not found", String::from_utf8_lossy(name))))? {
None => {
return Err(HairyError::wrap(HairyEvalError::TemplateNotFound(name),HairyLocationType::Call, context.template_name, line_no, column_no));
},
Some(values) => values,
};
} else if opcode == HairyBytecode::CallDynamicBody as u8 {
let hairy_template;
let value = expry_eval_func(BytecodeRef(name), &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, context.template_name, line_no, column_no))?;
if let DecodedValue::String(contents) = value {
hairy_template = contents;
} else if let DecodedValue::Null = value {
return Ok(());
} else {
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "expression in dynamic body template call should resolve to a string (or null), not {}", value.type_string())), HairyLocationType::Call, context.template_name, line_no, column_no));
}
result = decode_hairy(hairy_template)?;
} else if opcode == HairyBytecode::CallStatic as u8 {
result = match resolve_template(name, context.templates).map_err(|_| HairyEvalError::Error(write!(allocator, "template definitions problems: '{}' not found", String::from_utf8_lossy(name))))? {
None => {
return Err(HairyError::wrap(HairyEvalError::TemplateNotFound(name),HairyLocationType::Call, context.template_name, line_no, column_no));
},
Some(values) => values,
};
} else {
return Err(HairyError::wrap(HairyEvalError::Error("unrecognized loop opcode in template bytecode"),HairyLocationType::Call, context.template_name, line_no, column_no));
}
if result.args_count != args_count || args_count > MAX_TEMPLATE_ARGS {
return Err(HairyError::wrap(HairyEvalError::Error(write!(allocator, "call of template with {} arguments instead of expected {} arguments", args_count, result.args_count)), HairyLocationType::Call, context.template_name, line_no, column_no));
}
let mut values = Vec::new();
for _i in 0..args_count {
let expr = BytecodeRef(expression.read_var_string().map_err(err)?);
let retval = expry_slice_func(expr, &mut context.values, allocator, context.custom).map_err(|err| HairyError::wrap(HairyEvalError::Evaluate(write!(allocator, "{err}")), HairyLocationType::Call, context.template_name, line_no, column_no))?;
values.push(retval);
}
let mut templates = ScopedArrayBuilder::new(allocator);
templates.extend_from_slice(context.templates);
templates.push(RawReader::with(result.subtemplates.get()));
let templates = templates.build();
let mut context2 = Context { templates, template_name: result.name, custom: context.custom, escaper: context.escaper, values, };
evaluate_to(&mut RawReader::with(result.body.get()), &mut context2, allocator, output, depth-1).map_err(|err| err.add_location(HairyLocationType::Call, context.template_name, line_no, column_no))?;
continue;
}
return Err(HairyEvalError::Bytecode(write!(allocator, "unrecognized opcode {opcode}")).into());
}
Ok(())
}
pub fn hairy_eval<'a,'b,'c,'d,'e>(template_bytecode: BytecodeRef<'b>, options: &HairyEvalOptions<'b,'d,'e>, allocator: &mut MemoryScope<'c>) -> Result<Vec<&'b [u8]>, HairyError<'b>> where 'c: 'b, 'b: 'a, 'e: 'b {
let CompiledDefine{name, body, args_count, subtemplates, ..} = decode_hairy(template_bytecode.get())?;
if options.values.len() != args_count as usize {
return Err(HairyEvalError::Error(write!(allocator, "template called with wrong number of arguments (values), {} instead of {}", options.values.len(), args_count)).into());
}
let mut templates = ScopedArrayBuilder::new(allocator);
if let Some(given_templates) = &options.given_templates {
templates.extend_from_slice(given_templates);
}
templates.push(RawReader::with(subtemplates.get()));
let templates = templates.build();
let mut output : Vec<&'b [u8]> = Vec::with_capacity(128);
let mut context = Context { templates, template_name: name, custom: options.custom, escaper: options.escaper, values: options.values.to_vec() };
let mut bytecode_reader = RawReader::with(body.get());
evaluate_to(&mut bytecode_reader, &mut context, allocator, &mut output, MAX_TEMPLATE_RECURSION)?;
Ok(output)
}
pub fn hairy_eval_html(template_bytecode: BytecodeRef<'_>, options: &HairyEvalOptions) -> Result<Vec<u8>, String> {
let mut allocator = MemoryPool::new();
let mut scope = allocator.rewind();
let result = hairy_eval(template_bytecode, options, &mut scope);
match result {
Ok(result) => Ok(result.concat()),
Err(err) => Err(format!("{err}")),
}
}
const HAIRY_MAGIC : &[u8; 2] = b"hy";
fn decode_hairy(template_bytecode: &[u8]) -> Result<CompiledDefine, HairyError> {
let mut reader = RawReader::with(template_bytecode);
decode_hairy_reader(&mut reader)
}
fn decode_hairy_reader<'b>(reader: &mut RawReader<'b>) -> Result<CompiledDefine<'b>, HairyError<'b>> {
let header = reader.read_bytes(HAIRY_MAGIC.len())?;
if header != HAIRY_MAGIC {
return Err(HairyEvalError::Bytecode("expression is missing magic header ('hair')").into());
}
let name = reader.read_var_string()?;
let filename = reader.read_var_string()?;
let args_count = reader.read_var_u64()?;
let lazy_args = reader.read_var_string()?;
let body = BytecodeRef(reader.read_var_string()?);
let subtemplates = BytecodeRef(reader.read_var_string()?);
Ok(CompiledDefine{name, filename, args_count, lazy_arg_types: lazy_args, body, subtemplates })
}
#[cfg(test)]
mod hairy {
use std::io::Write;
use std::collections::BTreeMap;
use crate::*;
pub struct TestCustomFuncs {
}
impl<'a> CustomFuncs<'a> for TestCustomFuncs {
fn call<'b,'c>(&'_ self, name: &'_ [u8], _args: &'_ [DecodedValue<'b>], scope: &'_ mut MemoryScope<'c>) -> Result<DecodedValue<'b>,&'b str> where 'c: 'b {
if b"func" == name {
Ok(DecodedValue::String(scope.copy_u8("dyncustomfoo".as_bytes())))
} else {
Err("no custom functions defined except for 'func'")
}
}
fn types(&self) -> BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)> {
BTreeMap::from([
(key_str("func"), (vec![], ExpryType::String)),
])
}
}
#[test]
fn test_escape() {
let template = r"foobar = \{{=this.foovar .. this.barvar}}";
let value = value!({
"foovar": "foo",
"barvar": "bar",
}).encode_to_vec();
let mut options = HairyOptions::new();
options.set_named_dynamic_values(&[("this", value.to_ref())]);
let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
match result {
Ok(parsed) => {
match hairy_eval_html(parsed.to_ref(), &(&options).try_into().unwrap()) {
Ok(output) => {
assert_eq!(br#"foobar = {{=this.foovar .. this.barvar}}"#, &output[..]);
},
Err(err) => { eprintln!("{}", err); },
}
},
Err(err) => {
eprintln!("{}", err);
}
}
}
#[test]
fn example_html_interface() {
let template = r#"foobar = {{=this.foovar .. this.barvar}}"#;
let value = value!({
"foovar": "foo",
"barvar": "bar",
}).encode_to_vec();
let mut options = HairyOptions::new();
options.set_named_dynamic_values(&[("this", value.to_ref())]);
let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
match result {
Ok(parsed) => {
match hairy_eval_html(parsed.to_ref(), &(&options).try_into().unwrap()) {
Ok(output) => {
assert_eq!(b"foobar = foobar", &output[..]);
},
Err(err) => { eprintln!("{}", err); },
}
},
Err(err) => {
eprintln!("{}", err);
}
}
}
struct Foo {
foo: u32,
bar: bool,
}
impl<'a> From<&'a Foo> for DecodedValue<'a> {
fn from(v: &'a Foo) -> Self {
value!({
"foo": v.foo as i64,
"bar": v.bar,
})
}
}
#[test]
fn example_nesting() {
let main_template = r#"{{inline $sitetitle = "foooooobaaaar"}}
{{inline $arr = [1,2,3]}}
<html><title>{{=$sitetitle}}: {{=this.pagetitle}}</title><body>{{call ((this.bodytemplate))(this, this.body)}}</body></html>"#;
let mut options = HairyCompileOptions::new();
options.set_dynamic_value_name_and_types(&[("this", expry_type!("{pagetitle: string, foos: [{foo:int,bar:bool}], foo: {foo:int,bar:bool}, bodytemplate: string, body: {foobarvar: string}}"))]);
let main = hairy_compile_html(main_template, "main.tpl", &options);
if let Err(main) = &main {
eprintln!("{}", main);
}
let main = main.unwrap();
let child_template = r#"<p>title of this page = {{=this.pagetitle}}</p><p>foobar = {{=body.foobarvar}}"#;
options.set_dynamic_value_name_and_types(&[("this", expry_type!("{pagetitle: string, foos: [{foo:int,bar:bool}], foo: {foo:int,bar:bool}, bodytemplate: string, body: {foobarvar: string}}")), ("body", expry_type!("{foobarvar: string}"))]);
let child = hairy_compile_html(child_template, "child.tpl", &options);
if let Err(err) = &child {
println!("{}", err);
}
let child = child.unwrap();
let foos = vec![
Foo{foo:1,bar:true},
Foo{foo:2,bar:false},
];
let value = value!({
"bodytemplate": child,
"body": {"foobarvar": "foobar"},
"pagetitle": "my page",
"foo": Foo{foo:1,bar:true},
"foos": foos,
}).encode_to_vec();
let mut options = HairyEvalOptions::new();
options.values = vec![value.to_ref()];
assert_eq!("<html><title>foooooobaaaar: my page</title><body><p>title of this page = my page</p><p>foobar = foobar</body></html>", String::from_utf8_lossy(&hairy_eval_html(main.to_ref(), &options).unwrap()));
}
#[test]
fn basic() {
let hairy = r#"
Basic: {{="a"}},{{=1+2+3}}
Normally html is auto escaped (to prevent cross-site-scripting attacks):
{{="<p><b>Bold</b></p>"}}
But if you don't want that, you can add a different escape mode:
{{="<p><b>Bold</b></p>":none}}
Because these templates are aimed at humans, floats get rounded. All these expressions result in the output "1.0". Floats are rounded to 6 digits after the decimal seperator, doubles are rounded tot 14 digits after the decimal seperator. Other behaviour can be specified by using a different escaper or different escape modes.
float rounding: {{=0.9999998f}}
float rounding: {{=0.99999998f}}
double rounding: {{=0.999999999999996}}
double rounding: {{=1.0}}
float not rounded: {{=0.9999998f:unrounded}}
Conditional:
\{{if true}}
Show text
\{{else}}
Hidden
\{{end}}
Output:
{{if true}}
Show text
{{else}}
Hidden
{{end}}
Output:
{{if this.number>0???}}
Not shown, because condition triggers an error
{{else}}
Shown because condition triggers an error which is catched with the `???` operator.
{{endif}}
Loop over array:
\{{for i in this.numbers}}
- \{{=i}}
\{{end}}
Output:
{{for i in this.numbers}}
- {{=i}}
{{end}}
Loop over array with objects, using an index variable (with `\{{for (index,person) in this.persons}}`):
{{for (index,person) in this.persons}}
- {{=index}}={{=person.name}}
{{end}}
Call a template, with a specific context specified after the `<-`. See next example how to define one:
{{call card(false,"Some card", "Explanation over some card.")}}
If the template name is prefixed with one `*`, the template name can be an expression that is evaluated to a template name:
{{call ("CARD".lower())(false, "Some card", "Explanation over some card.")}}
If the template name is prefixed with `**`, the template name can be an expression that is evaluated to template binary code. This is useful to embed templates in one each other. See another example for this.
Define a template. The template can optionally have default values, that are specified after `defaults`. These default values are only evaluated once during compile time (so they do not depend on the context a template is evaluted in).
{{define card(dark:bool,name:string,body:string)}}
<div class="card-{{=dark?"dark":"light"}}">
<h2>{{=name}}</h2>
<p>{{=body}}</p>
</div>
{{enddefine}}
"#;
let mut allocator = MemoryPool::new();
let mut scope = allocator.rewind();
let maincode = r#"
<html>
<title extra="{{=this.title}}">{{=this.title}}</title>
<script>{{=this.title}}</script>
<script>var foo = {{=this.persons:js}};</script>
<body>
{{call ((this.content))(this.numbers,this.persons,this.number)}}
</body>
</html>"#;
let mut main = parse_html(maincode, 0, &mut scope);
if let Err(err) = &mut main {
println!("{}", hairy_compile_error_format_console(err).1);
}
let mut options = HairyCompileOptions::new();
options.set_dynamic_value_name_and_types(&[("this", expry_type!("{title:string,persons:[{name:string}],content:string,numbers:[int],number:int}"))]);
options.custom_types = TestCustomFuncs{}.types();
let mut main = hairy_compile(maincode, "main.tpl", &options, &mut scope, Some(main.unwrap()));
if let Err(err) = &mut main {
println!("{}", hairy_compile_error_format_console(err).1);
}
let main = main.unwrap();
let mut escaping = parse_html(hairy, 0, &mut scope);
if let Err(err) = &mut escaping {
println!("{}", hairy_compile_error_format_console(err).1);
}
let escaping = escaping.unwrap();
let mut options = HairyOptions::new();
let value = value!({
"title": "My title<script>",
"content": main,
"numbers": [1,2,3,4],
"persons": [{"name": "Andrew"},{"name": "Bart"},{"name": "Casper"}],
"number": 1,
}).encode_to_scope(&mut scope);
options.set_named_dynamic_values(&[
("this", value),
]);
options.custom(&TestCustomFuncs{});
let mut test = hairy_compile(hairy, "test.tpl", &(&options).try_into().unwrap(), &mut scope, Some(escaping));
if let Err(err) = &mut test {
println!("{}", hairy_compile_error_format_console(err).1);
}
let test = test.unwrap();
if false {
for _ in 0..16 {
let before = std::time::Instant::now();
let count = 4*16384;
for _ in 0..count {
let mut scope = scope.rewind();
let output = hairy_eval(main.to_ref(), &(&options).try_into().unwrap(), &mut scope).unwrap();
assert!(!output.is_empty());
}
let after = std::time::Instant::now();
let dur = after - before;
eprintln!("{}x in {} ms", count, dur.as_millis());
}
} else {
let output = hairy_eval(test.to_ref(), &(&options).try_into().unwrap(), &mut scope);
let output = output.unwrap();
for c in output {
print!("{}", std::str::from_utf8(c).unwrap());
}
println!();
}
}
use expry_macros::*;
#[test]
fn compile_time_expr() {
let compiled = expry!("2+3");
if cfg!(feature = "mini") {
assert_eq!(compiled.get().len(), 13);
} else {
assert_eq!(compiled.get().len(), 2+3);
}
let mut allocator = MemoryPool::new();
let mut scope = allocator.rewind();
let result = expry_eval(compiled, &mut vec![], &mut scope);
let result = result.unwrap();
assert_eq!(result, value!(5));
}
#[test]
fn compile_time_type_expr() {
let result = expry_type!("int?");
assert_eq!(result, ExpryType::Nullable(Box::new(ExpryType::Int)));
}
#[test]
fn expry_types() {
assert!(!expry_type!("{foo: string}").used_as(&expry_type!("{foo: string, bar: string}")));
assert!(expry_type!("{foo: string,bar:string}").used_as(&expry_type!("{foo: string}")));
assert!(expry_type!("{*: {foo: string, bar: string}}").used_as(&expry_type!("{*: {foo: string}}")));
assert!(!expry_type!("{*: {parent:string?,title:string,}}").used_as(&expry_type!("{*: {parent?:string,title:string,}}")));
assert!(expry_type!("{*: {parent:string,title:string,}}").used_as(&expry_type!("{*: {parent?:string,title:string,}}")));
assert!(!expry_type!("{*: {parent?:string?,title:string,}}").used_as(&expry_type!("{*: {parent:string,title:string,}}")));
}
#[test]
fn embedded() {
let main_template = r#"<html><title>{{=this.title}}</title><body>{{call ((this.body))(this.title,this.foobarvar)}}</body></html>"#;
let mut options = HairyCompileOptions::new();
options.set_dynamic_value_name_and_types(&[("this", expry_type!("{title: string, foobarvar: string, body: string}"))]);
let main = hairy_compile_html(main_template, "main.tpl", &options);
if let Err(err) = &main {
println!("{}", err);
}
let main = main.unwrap();
let child_template = r#"<p>title of this page = {{=title}}</p><p>foobar = {{=foobarvar}}"#;
options.set_dynamic_value_name_and_types(&[("title", expry_type!("string")),("foobarvar",expry_type!("string"))]);
let child = hairy_compile_html(child_template, "child.tpl", &options).unwrap();
let value = value!({
"body": child,
"foobarvar": "foobar",
"title": "my title",
}).encode_to_vec();
let mut options = HairyEvalOptions::new();
options.values = vec![value.to_ref()];
match hairy_eval_html(main.to_ref(), &options) {
Ok(output) => { std::io::stdout().write_all(&output).unwrap(); },
Err(err) => {
eprintln!("{}", err);
panic!();
},
}
}
#[test]
fn inline_script() {
let template = r#"{{inline $sitetitle = true ? "t" : "f"}}
{{inline foo = 43}}
<html><title>{{=$sitetitle}}: title</title><body><script>
document.innerHTML = '<button>Open in App</button>';
</script></body></html>
<!-- <foo {{=this.title}}> -->
<{{=this.title}}>
{{=this.foo}}
"#;
let mut allocator = MemoryPool::new();
let mut scope = allocator.rewind();
let parsed = parse_html(template, 0, &mut scope);
if let Err(msg) = &parsed {
println!("error: {}", msg);
}
let value = value!({"title": "<b>title</b>", "foo": 123}).encode_to_vec();
let mut options = HairyOptions::new();
options.set_named_dynamic_values(&[("this", value.to_ref())]);
let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
if let Err(msg) = &result {
println!("error: {}", msg);
}
let main = result.unwrap();
println!("{}", String::from_utf8_lossy(&hairy_eval_html(main.to_ref(), &(&options).try_into().unwrap()).unwrap()));
}
#[test]
fn inline_script2() {
let template_works = r#"<html><title>title</title><body>
{{if true}}
Show text
{{else}}
{{end}}
</body></html>1"#;
let mut compile_options = HairyCompileOptions::new();
compile_options.set_dynamic_value_count(0);
let result = hairy_compile_html(template_works, "test.tpl", &compile_options);
if let Err(msg) = &result {
println!("{}", msg);
}
result.unwrap();
let template2 = r#"<html><title>title</title><body>
{{if true}}
Show text
{{else}}
{{end}}
</body></html>2"#;
let result = hairy_compile_html(template2, "test.tpl", &compile_options);
if let Err(msg) = &result {
println!("{}", msg);
}
result.unwrap();
let template3 = r#"<html><title>title</title><body>
{{if true}}
Show text
{{end}}
</body></html>3"#;
let result = hairy_compile_html(template3, "test.tpl", &compile_options);
if let Err(msg) = &result {
println!("{}", msg);
}
result.unwrap();
}
pub struct TranslateFuncs {
map: std::collections::HashMap<String,String>,
}
impl<'a> CustomFuncs<'a> for TranslateFuncs {
fn call<'b,'c>(&self, name: &'_ [u8], args: &'_ [DecodedValue<'b>], scope: &'_ mut MemoryScope<'c>) -> Result<DecodedValue<'b>,&'b str> where 'c: 'b {
if name == b"tr" {
if let (Some(DecodedValue::String(key)), Some(DecodedValue::String(default))) = (args.first(), args.get(1)) {
let key = core::str::from_utf8(key);
if let Ok(key) = key {
if let Some(translated) = self.map.get(key) {
Ok(DecodedValue::String(scope.copy_u8(translated.as_bytes())))
} else {
Ok(DecodedValue::String(default))
}
} else {
Err("tr(key, default) with invalid UTF-8 as key")
}
} else {
Err("tr(key, default) expected")
}
} else {
Err("no custom functions defined except for 'func'")
}
}
fn types(&self) -> BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)> {
BTreeMap::from([
(key_str("tr"), (vec![ExpryType::String, ExpryType::String], ExpryType::String)),
])
}
}
pub struct FallbackTranslationsFuncs {
}
impl<'a> CustomFuncs<'a> for FallbackTranslationsFuncs {
fn call<'b,'c>(&self, name: &'_ [u8], args: &'_ [DecodedValue<'b>], _scope: &'_ mut MemoryScope<'c>) -> Result<DecodedValue<'b>,&'b str> where 'c: 'b {
if name == b"tr" {
if let (_, Some(DecodedValue::String(default))) = (args.first(), args.get(1)) {
Ok(DecodedValue::String(default))
} else {
Err("tr(key, default) expected")
}
} else {
Err("no custom functions defined except for 'tr'")
}
}
fn types(&self) -> BTreeMap<Key<'static>,(Vec<ExpryType>,ExpryType)> {
BTreeMap::from([
(key_str("tr"), (vec![ExpryType::String, ExpryType::String], ExpryType::String)),
])
}
}
#[test]
pub fn translations() {
let mut map = std::collections::HashMap::new();
map.insert("hello".to_string(), "Hallo.".to_string());
map.insert("welcome-text".to_string(), "Welkom op onze site, fijn dat je ons virtueel bezoekt.".to_string());
let template = r#"
<h1>{{=tr("hello", "Hello.")}}</h1>
<p>{{=tr("welcome-text", "Welcome to our site. We are glad that you are here.")}}</p>
"#;
let mut options = HairyOptions::new();
options.strip_spaces = true;
let custom = TranslateFuncs { map, };
options.custom(&custom);
let result = hairy_compile_html(template, "test.tpl", &(&options).try_into().unwrap());
if let Err(msg) = &result {
println!("error: {}", msg);
}
let main = result.unwrap();
let output = hairy_eval_html(main.to_ref(), &(&options).try_into().unwrap());
if let Err(msg) = &output {
println!("error: {}", msg);
}
let output = output.unwrap();
assert_eq!(r#" <h1>Hallo.</h1> <p>Welkom op onze site, fijn dat je ons virtueel bezoekt.</p> "#.as_bytes(), output);
let mut eval_options = HairyEvalOptions::new();
eval_options.custom(&FallbackTranslationsFuncs{});
let output = hairy_eval_html(main.to_ref(), &eval_options);
if let Err(msg) = &output {
println!("error: {}", msg);
}
let output = output.unwrap();
assert_eq!(r#" <h1>Hello.</h1> <p>Welcome to our site. We are glad that you are here.</p> "#.as_bytes(), output);
}
}