use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
use crate::value::VmValue;
mod ast;
mod error;
mod expr_parser;
mod filters;
mod lexer;
mod parser;
mod render;
#[cfg(test)]
mod tests;
use error::TemplateError;
use parser::parse;
use render::{render_nodes, RenderCtx, Scope};
thread_local! {
static PROMPT_REGISTRY: RefCell<Vec<RegisteredPrompt>> = const { RefCell::new(Vec::new()) };
static PROMPT_RENDER_INDICES: RefCell<BTreeMap<String, Vec<u64>>> =
const { RefCell::new(BTreeMap::new()) };
static PROMPT_RENDER_ORDINAL: RefCell<u64> = const { RefCell::new(0) };
}
const PROMPT_REGISTRY_CAP: usize = 64;
#[derive(Debug, Clone)]
pub struct RegisteredPrompt {
pub prompt_id: String,
pub template_uri: String,
pub rendered: String,
pub spans: Vec<PromptSourceSpan>,
}
pub(crate) fn register_prompt(
template_uri: String,
rendered: String,
spans: Vec<PromptSourceSpan>,
) -> String {
let prompt_id = format!("prompt-{}", next_prompt_serial());
PROMPT_REGISTRY.with(|reg| {
let mut reg = reg.borrow_mut();
if reg.len() >= PROMPT_REGISTRY_CAP {
reg.remove(0);
}
reg.push(RegisteredPrompt {
prompt_id: prompt_id.clone(),
template_uri,
rendered,
spans,
});
});
prompt_id
}
thread_local! {
static PROMPT_SERIAL: RefCell<u64> = const { RefCell::new(0) };
}
fn next_prompt_serial() -> u64 {
PROMPT_SERIAL.with(|s| {
let mut s = s.borrow_mut();
*s += 1;
*s
})
}
pub fn lookup_prompt_span(
prompt_id: &str,
output_offset: usize,
) -> Option<(String, PromptSourceSpan)> {
PROMPT_REGISTRY.with(|reg| {
let reg = reg.borrow();
let entry = reg.iter().find(|p| p.prompt_id == prompt_id)?;
let best = entry
.spans
.iter()
.filter(|s| {
output_offset >= s.output_start
&& output_offset < s.output_end.max(s.output_start + 1)
})
.min_by_key(|s| {
let width = s.output_end.saturating_sub(s.output_start);
let kind_weight = match s.kind {
PromptSpanKind::Expr => 0,
PromptSpanKind::LegacyBareInterp => 1,
PromptSpanKind::Text => 2,
PromptSpanKind::Include => 3,
PromptSpanKind::ForIteration => 4,
PromptSpanKind::If => 5,
};
(kind_weight, width)
})?
.clone();
Some((entry.template_uri.clone(), best))
})
}
pub fn lookup_prompt_consumers(
template_uri: &str,
template_line_start: usize,
template_line_end: usize,
) -> Vec<(String, PromptSourceSpan)> {
PROMPT_REGISTRY.with(|reg| {
let reg = reg.borrow();
reg.iter()
.filter(|p| p.template_uri == template_uri)
.flat_map(|p| {
let prompt_id = p.prompt_id.clone();
p.spans
.iter()
.filter(move |s| {
let line = s.template_line;
line > 0 && line >= template_line_start && line <= template_line_end
})
.cloned()
.map(move |s| (prompt_id.clone(), s))
})
.collect()
})
}
pub fn record_prompt_render_index(prompt_id: &str, event_index: u64) {
PROMPT_RENDER_INDICES.with(|map| {
map.borrow_mut()
.entry(prompt_id.to_string())
.or_default()
.push(event_index);
});
}
pub fn next_prompt_render_ordinal() -> u64 {
PROMPT_RENDER_ORDINAL.with(|c| {
let mut n = c.borrow_mut();
*n += 1;
*n
})
}
pub fn prompt_render_indices(prompt_id: &str) -> Vec<u64> {
PROMPT_RENDER_INDICES.with(|map| map.borrow().get(prompt_id).cloned().unwrap_or_default())
}
pub(crate) fn reset_prompt_registry() {
PROMPT_REGISTRY.with(|reg| reg.borrow_mut().clear());
PROMPT_SERIAL.with(|s| *s.borrow_mut() = 0);
PROMPT_RENDER_INDICES.with(|map| map.borrow_mut().clear());
PROMPT_RENDER_ORDINAL.with(|c| *c.borrow_mut() = 0);
}
pub fn validate_template_syntax(src: &str) -> Result<(), String> {
parse(src).map(|_| ()).map_err(|e| e.message())
}
pub(crate) fn render_template_result(
template: &str,
bindings: Option<&BTreeMap<String, VmValue>>,
base: Option<&Path>,
source_path: Option<&Path>,
) -> Result<String, TemplateError> {
let (rendered, _spans) =
render_template_with_provenance(template, bindings, base, source_path, false)?;
Ok(rendered)
}
#[derive(Debug, Clone)]
pub struct PromptSourceSpan {
pub template_line: usize,
pub template_col: usize,
pub output_start: usize,
pub output_end: usize,
pub kind: PromptSpanKind,
pub bound_value: Option<String>,
pub parent_span: Option<Box<PromptSourceSpan>>,
pub template_uri: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptSpanKind {
Text,
Expr,
LegacyBareInterp,
If,
ForIteration,
Include,
}
pub(crate) fn render_template_with_provenance(
template: &str,
bindings: Option<&BTreeMap<String, VmValue>>,
base: Option<&Path>,
source_path: Option<&Path>,
collect_provenance: bool,
) -> Result<(String, Vec<PromptSourceSpan>), TemplateError> {
let nodes = parse(template).map_err(|mut e| {
if let Some(p) = source_path {
e.path = Some(p.to_path_buf());
}
e
})?;
let mut out = String::with_capacity(template.len());
let mut scope = Scope::new(bindings);
let include_root = base.map(|path| path.canonicalize().unwrap_or_else(|_| path.to_path_buf()));
let mut rc = RenderCtx {
base: base.map(Path::to_path_buf),
include_root,
include_stack: Vec::new(),
current_path: source_path.map(Path::to_path_buf),
current_include_parent: None,
};
let mut spans = if collect_provenance {
Some(Vec::new())
} else {
None
};
render_nodes(&nodes, &mut scope, &mut rc, &mut out, spans.as_mut()).map_err(|mut e| {
if e.path.is_none() {
e.path = source_path.map(Path::to_path_buf);
}
e
})?;
Ok((out, spans.unwrap_or_default()))
}