Add misses-rt crate, component example, README, and fix codegen for non-keyword literal tokens

- misses-rt: add Buffer/Markup HTML helpers, name/type helpers, parse_sections/parse_kv, and template AST
- codegen: add is_syn_keyword() — non-keyword idents in patterns now generate parse+compare instead of syn::Token![…]
- parser: split punct vs ident chars in literal pattern tokens so `props:` becomes two tokens
- examples: add component.mrs/.rs demonstrating named literal tokens and tt capture
- README: comprehensive guide covering .mrs syntax, running the compiler, misses-rt helpers, and examples

https://claude.ai/code/session_01Tppkab4eLsL21asHGjQJoF
This commit is contained in:
Claude
2026-03-07 19:23:53 +00:00
parent f90953a3bb
commit e8d835e196
11 changed files with 1156 additions and 21 deletions
+207
View File
@@ -0,0 +1,207 @@
pub mod template;
pub mod parse_helpers;
pub use template::{TemplateAst, parse_template};
pub use parse_helpers::{SectionMap, KvMap, parse_sections, parse_kv};
// ── Runtime types ────────────────────────────────────────────
/// Accumulation buffer for HTML rendering.
pub struct Buffer {
inner: String,
}
impl Buffer {
pub fn new() -> Self {
Buffer { inner: String::new() }
}
/// Push literal HTML (already safe / already escaped).
pub fn push_str(&mut self, s: &str) {
self.inner.push_str(s);
}
/// Push a user-supplied value, HTML-escaping it.
pub fn push_escaped(&mut self, s: &str) {
for ch in s.chars() {
match ch {
'&' => self.inner.push_str("&"),
'<' => self.inner.push_str("&lt;"),
'>' => self.inner.push_str("&gt;"),
'"' => self.inner.push_str("&quot;"),
'\'' => self.inner.push_str("&#x27;"),
c => self.inner.push(c),
}
}
}
}
impl Default for Buffer {
fn default() -> Self { Self::new() }
}
/// A string of trusted HTML markup produced by a component.
pub struct Markup {
inner: String,
}
impl Markup {
pub fn from_buffer(buf: Buffer) -> Self {
Markup { inner: buf.inner }
}
pub fn into_string(self) -> String {
self.inner
}
pub fn as_str(&self) -> &str {
&self.inner
}
}
impl std::fmt::Display for Markup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.inner)
}
}
// ── Name helpers ─────────────────────────────────────────────
/// `BlogPost` → `blog_posts` (table name: snake_case + plural)
pub fn to_table_name(ident: &syn::Ident) -> String {
let snake = pascal_to_snake(&ident.to_string());
format!("{}s", snake)
}
/// `BlogPost` → `blog_post`
pub fn to_snake_case(ident: &syn::Ident) -> String {
pascal_to_snake(&ident.to_string())
}
/// Works on a `&str` (useful inside codegen where you have String/&str).
pub fn to_snake_case_str(s: &str) -> String {
pascal_to_snake(s)
}
/// `blog_post` → `BlogPost` (returns a `proc_macro2::Ident` for use in quote!)
pub fn to_pascal_case(s: &str) -> proc_macro2::Ident {
let pascal = snake_to_pascal(s);
quote::format_ident!("{}", pascal)
}
/// Join parts into a single snake_case identifier.
/// Each part that looks like PascalCase is first converted to snake_case.
pub fn concat_ident(parts: &[&str]) -> proc_macro2::Ident {
let snake_parts: Vec<String> = parts.iter().map(|p| pascal_to_snake(p)).collect();
let joined = snake_parts.join("_");
quote::format_ident!("{}", joined)
}
// ── Type helpers ─────────────────────────────────────────────
/// Returns `true` if `ty` is `Option<T>` for any `T`.
pub fn is_option(ty: &syn::Type) -> bool {
unwrap_option(ty).is_some()
}
/// If `ty` is `Option<T>`, returns `Some(&T)`, else `None`.
pub fn unwrap_option(ty: &syn::Type) -> Option<&syn::Type> {
if let syn::Type::Path(tp) = ty {
let seg = tp.path.segments.last()?;
if seg.ident != "Option" {
return None;
}
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return Some(inner);
}
}
}
None
}
/// Best-effort mapping of a Rust type to a SQL column type string.
pub fn to_sql_type(ty: &syn::Type) -> String {
let s = quote::quote!(#ty).to_string();
let s = s.replace(' ', "");
match s.as_str() {
"i8"|"i16"|"i32"|"i64"|"u8"|"u16"|"u32"|"u64"|"usize"|"isize" => "INTEGER".into(),
"f32"|"f64" => "REAL".into(),
"bool" => "BOOLEAN".into(),
"String"|"&str"|"&'staticstr" => "TEXT".into(),
_ if s.starts_with("Option<") => {
// unwrap and recurse
if let Some(inner) = unwrap_option(ty) {
to_sql_type(inner) + " NULL"
} else {
"TEXT NULL".into()
}
}
_ => "BLOB".into(),
}
}
// ── Internal conversion utilities ────────────────────────────
fn pascal_to_snake(s: &str) -> String {
if s.is_empty() {
return String::new();
}
let mut out = String::new();
let mut prev_upper = false;
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c.is_uppercase() {
let next_is_lower = chars.get(i + 1).map(|c| c.is_lowercase()).unwrap_or(false);
if i > 0 && (!prev_upper || next_is_lower) {
out.push('_');
}
out.push(c.to_lowercase().next().unwrap());
prev_upper = true;
} else {
out.push(c);
prev_upper = false;
}
}
out
}
fn snake_to_pascal(s: &str) -> String {
s.split('_')
.filter(|p| !p.is_empty())
.map(|p| {
let mut c = p.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + c.as_str(),
}
})
.collect()
}
// ── Tests ─────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snake_cases() {
assert_eq!(pascal_to_snake("BlogPost"), "blog_post");
assert_eq!(pascal_to_snake("UserID"), "user_id");
assert_eq!(pascal_to_snake("HTTPSRequest"), "https_request");
assert_eq!(pascal_to_snake("simple"), "simple");
}
#[test]
fn pascal_cases() {
assert_eq!(snake_to_pascal("blog_post"), "BlogPost");
assert_eq!(snake_to_pascal("user_id"), "UserId");
}
#[test]
fn table_names() {
let ident = quote::format_ident!("BlogPost");
assert_eq!(to_table_name(&ident), "blog_posts");
}
}
+130
View File
@@ -0,0 +1,130 @@
use std::collections::HashMap;
use proc_macro2::TokenStream;
// ── SectionMap ────────────────────────────────────────────────
/// A map from section name → TokenStream, parsed from a token tree like:
/// ```text
/// fields { ... }
/// methods { ... }
/// ```
pub struct SectionMap {
sections: HashMap<String, TokenStream>,
}
impl SectionMap {
/// Get the token stream for a named section. Returns empty stream if absent.
pub fn get(&self, name: &str) -> TokenStream {
self.sections.get(name).cloned().unwrap_or_default()
}
/// Returns true if the section exists (even if empty).
pub fn has(&self, name: &str) -> bool {
self.sections.contains_key(name)
}
/// Iterate over all (name, tokens) pairs.
pub fn iter(&self) -> impl Iterator<Item = (&str, &TokenStream)> {
self.sections.iter().map(|(k, v)| (k.as_str(), v))
}
}
/// Parse a token stream as named sections: `name { ... } name { ... }`.
///
/// # Example
/// Input tokens: `fields { x: u32, y: u32 } methods { fn sum(&self) -> u32 { ... } }`
/// Result: `{ "fields" → tokens, "methods" → tokens }`
pub fn parse_sections(tokens: TokenStream, _sections: &[&str]) -> SectionMap {
use proc_macro2::{TokenTree, Delimiter};
let mut sections = HashMap::new();
let mut iter = tokens.into_iter().peekable();
while let Some(tt) = iter.next() {
// Expect an Ident (section name)
let name = match &tt {
TokenTree::Ident(id) => id.to_string(),
_ => continue,
};
// Expect the next token to be a braced group
match iter.peek() {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
let group = match iter.next() {
Some(TokenTree::Group(g)) => g,
_ => unreachable!(),
};
sections.insert(name, group.stream());
}
_ => {}
}
}
SectionMap { sections }
}
// ── KvMap ─────────────────────────────────────────────────────
/// A map of key = value pairs parsed from a token stream like:
/// ```text
/// table = "users", primary_key = id, nullable = true
/// ```
pub struct KvMap {
entries: HashMap<String, String>,
}
impl KvMap {
/// Get a value by key (as the string representation of the token(s)).
pub fn get(&self, key: &str) -> Option<&str> {
self.entries.get(key).map(|s| s.as_str())
}
pub fn get_str(&self, key: &str) -> Option<&str> {
self.get(key)
}
/// Returns true if the key is present.
pub fn has(&self, key: &str) -> bool {
self.entries.contains_key(key)
}
}
/// Parse `key = value, key = value, ...` from a token stream.
pub fn parse_kv(tokens: TokenStream) -> KvMap {
use proc_macro2::TokenTree;
let mut entries = HashMap::new();
let tts: Vec<TokenTree> = tokens.into_iter().collect();
let mut i = 0;
while i < tts.len() {
// key: must be an ident
let key = match &tts[i] {
TokenTree::Ident(id) => id.to_string(),
_ => { i += 1; continue; }
};
i += 1;
// skip whitespace (handled by tokenizer) — expect `=`
if i >= tts.len() { break; }
match &tts[i] {
TokenTree::Punct(p) if p.as_char() == '=' => { i += 1; }
_ => continue,
}
// value: collect tokens until `,` or end
let mut value_parts: Vec<String> = Vec::new();
while i < tts.len() {
match &tts[i] {
TokenTree::Punct(p) if p.as_char() == ',' => { i += 1; break; }
tt => {
value_parts.push(tt.to_string());
i += 1;
}
}
}
entries.insert(key, value_parts.join(" "));
}
KvMap { entries }
}
+485
View File
@@ -0,0 +1,485 @@
use proc_macro2::{TokenStream, TokenTree, Delimiter};
// ── Public surface ────────────────────────────────────────────
pub struct TemplateAst {
pub nodes: Vec<TemplateNode>,
}
/// Parse a proc_macro2::TokenStream containing HTML-ish template markup.
///
/// Recognised constructs:
/// - `{{ expr }}` → escaped interpolation (→ push_escaped)
/// - `{{{ expr }}}` → raw interpolation (→ push_str of .to_string())
/// - `<tag @if="cond">…</tag>` → conditional block
/// - `<tag @for="x in y">…</tag>` → loop block
/// - `<tag @else>…</tag>` → else branch
/// - `<Component …/>` → component rendering call (PascalCase tag)
/// - `<tag attr="val">…</tag>` → HTML element
/// - everything else → emitted as literal text
pub fn parse_template(tokens: TokenStream) -> TemplateAst {
let flat = flatten(tokens);
let (nodes, _) = parse_nodes(&flat, 0, None);
TemplateAst { nodes }
}
// ── AST types ─────────────────────────────────────────────────
#[derive(Debug, Clone)]
pub enum TemplateNode {
Text(String),
Escape { expr: String },
Raw { expr: String },
Element(Box<Element>),
}
#[derive(Debug, Clone)]
pub struct Element {
pub tag: String,
pub is_component: bool,
pub attrs: Vec<(String, String)>,
pub directives: Vec<Directive>,
pub children: Vec<TemplateNode>,
pub self_closing: bool,
}
#[derive(Debug, Clone)]
pub enum Directive {
If(String),
For { var: String, iter: String },
Else,
}
// ── Flat token representation ─────────────────────────────────
#[derive(Debug, Clone)]
enum Tok {
Lt,
Gt,
Slash,
At,
Eq,
Comma,
Ident(String),
Lit(String), // string literal — includes the outer quotes
BraceGroup(Vec<Tok>), // { ... }
Other(String),
}
fn flatten(ts: TokenStream) -> Vec<Tok> {
ts.into_iter().flat_map(flatten_tt).collect()
}
fn flatten_tt(tt: TokenTree) -> Vec<Tok> {
match tt {
TokenTree::Ident(i) => vec![Tok::Ident(i.to_string())],
TokenTree::Literal(l) => vec![Tok::Lit(l.to_string())],
TokenTree::Punct(p) => vec![match p.as_char() {
'<' => Tok::Lt,
'>' => Tok::Gt,
'/' => Tok::Slash,
'@' => Tok::At,
'=' => Tok::Eq,
',' => Tok::Comma,
c => Tok::Other(c.to_string()),
}],
TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
vec![Tok::BraceGroup(flatten(g.stream()))]
}
TokenTree::Group(g) => {
// Other delimiters (parens, brackets) — flatten inline with pseudo-tokens
let (open, close) = match g.delimiter() {
Delimiter::Parenthesis => ("(", ")"),
Delimiter::Bracket => ("[", "]"),
_ => ("{", "}"),
};
let mut v = vec![Tok::Other(open.to_string())];
v.extend(flatten(g.stream()));
v.push(Tok::Other(close.to_string()));
v
}
}
}
// ── Parser ────────────────────────────────────────────────────
/// Returns (nodes, next_pos).
/// `stop_at` — stop when we encounter `</stop_at>`.
fn parse_nodes(toks: &[Tok], mut pos: usize, stop_at: Option<&str>) -> (Vec<TemplateNode>, usize) {
let mut nodes: Vec<TemplateNode> = Vec::new();
while pos < toks.len() {
// ── {{ expr }} / {{{ expr }}} interpolation ──────────
if let Tok::BraceGroup(inner) = &toks[pos] {
// Check for nested brace group → {{ }}
if inner.len() == 1 {
if let Tok::BraceGroup(inner2) = &inner[0] {
// {{{ }}} → raw
if inner2.len() == 1 {
if let Tok::BraceGroup(expr_toks) = &inner2[0] {
let expr = toks_to_str(expr_toks);
nodes.push(TemplateNode::Raw { expr });
pos += 1;
continue;
}
}
// {{ }} → escaped
let expr = toks_to_str(inner2);
nodes.push(TemplateNode::Escape { expr });
pos += 1;
continue;
}
}
// Other brace group → treat as text `{ ... }`
let text = format!("{{ {} }}", toks_to_str(inner));
push_text(&mut nodes, &text);
pos += 1;
continue;
}
// ── HTML tag ─────────────────────────────────────────
if let Tok::Lt = &toks[pos] {
// closing tag?
if pos + 1 < toks.len() {
if let Tok::Slash = &toks[pos + 1] {
// </tag> — check if it matches our stop_at
if let Some(stop) = stop_at {
if let Some(Tok::Ident(tag)) = toks.get(pos + 2) {
if tag == stop {
// consume </tag>
let end = find_gt(toks, pos + 3).unwrap_or(pos + 3);
pos = end + 1;
return (nodes, pos);
}
}
}
// Unknown closing tag → emit as text
let end = find_gt(toks, pos + 1).unwrap_or(pos + 1);
let text = reconstruct_tag(toks, pos, end);
push_text(&mut nodes, &text);
pos = end + 1;
continue;
}
}
// Opening / self-closing tag
if let Some((elem, next_pos)) = parse_element(toks, pos) {
let tag_name = elem.tag.clone();
let is_self = elem.self_closing;
let mut elem = elem;
if !is_self {
// parse children until </tag>
let (children, after) = parse_nodes(toks, next_pos, Some(&tag_name));
elem.children = children;
pos = after;
} else {
pos = next_pos;
}
nodes.push(TemplateNode::Element(Box::new(elem)));
continue;
}
// Couldn't parse as element — treat `<` as text
push_text(&mut nodes, "<");
pos += 1;
continue;
}
// ── Anything else → text ─────────────────────────────
let text = tok_to_str(&toks[pos]);
push_text(&mut nodes, &text);
pos += 1;
}
(nodes, pos)
}
/// Parse an element starting at `<`. Returns (Element, pos_after_closing_`>`).
fn parse_element(toks: &[Tok], start: usize) -> Option<(Element, usize)> {
debug_assert!(matches!(toks.get(start), Some(Tok::Lt)));
let mut pos = start + 1;
// tag name
let tag = match toks.get(pos) {
Some(Tok::Ident(t)) => t.clone(),
_ => return None,
};
pos += 1;
let is_component = tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
// attributes + directives
let mut attrs: Vec<(String, String)> = Vec::new();
let mut directives: Vec<Directive> = Vec::new();
let mut self_closing = false;
loop {
match toks.get(pos) {
None | Some(Tok::Gt) => {
if matches!(toks.get(pos), Some(Tok::Gt)) { pos += 1; }
break;
}
Some(Tok::Slash) => {
// /> self-closing
self_closing = true;
if matches!(toks.get(pos + 1), Some(Tok::Gt)) { pos += 2; } else { pos += 1; }
break;
}
Some(Tok::At) => {
// directive: @name or @name="value"
pos += 1;
let name = match toks.get(pos) {
Some(Tok::Ident(n)) => n.clone(),
_ => continue,
};
pos += 1;
if matches!(toks.get(pos), Some(Tok::Eq)) {
pos += 1;
let val = match toks.get(pos) {
Some(Tok::Lit(s)) => unquote(s),
Some(other) => tok_to_str(other),
None => String::new(),
};
pos += 1;
match name.as_str() {
"if" => directives.push(Directive::If(val)),
"for" => {
let (var, iter) = split_for_expr(&val);
directives.push(Directive::For { var, iter });
}
_other => attrs.push((format!("@{name}"), val)),
}
} else {
match name.as_str() {
"else" => directives.push(Directive::Else),
_ => {}
}
}
}
Some(Tok::Ident(name)) => {
// regular attribute: name or name="value"
let name = name.clone();
pos += 1;
if matches!(toks.get(pos), Some(Tok::Eq)) {
pos += 1;
let val = match toks.get(pos) {
Some(Tok::Lit(s)) => unquote(s),
Some(other) => tok_to_str(other),
None => String::new(),
};
pos += 1;
attrs.push((name, val));
} else {
attrs.push((name, String::new()));
}
}
_ => { pos += 1; }
}
}
Some((Element { tag, is_component, attrs, directives, children: vec![], self_closing }, pos))
}
// ── Codegen ───────────────────────────────────────────────────
impl TemplateAst {
/// Returns the loop variable names used in `@for` directives.
pub fn for_bindings(&self) -> Vec<String> {
let mut out = Vec::new();
collect_for_bindings(&self.nodes, &mut out);
out
}
/// Produce the body of a `render()` function as a TokenStream:
/// ```rust
/// let mut __buf = misses_rt::Buffer::new();
/// // ... push_str / push_escaped / if / for ...
/// misses_rt::Markup::from_buffer(__buf)
/// ```
pub fn codegen(&self) -> TokenStream {
let mut code = String::new();
code.push_str("let mut __buf = misses_rt :: Buffer :: new () ;\n");
for node in &self.nodes {
codegen_node(&mut code, node, 0);
}
code.push_str("misses_rt :: Markup :: from_buffer ( __buf )\n");
code.parse()
.unwrap_or_else(|_| TokenStream::new())
}
}
fn collect_for_bindings(nodes: &[TemplateNode], out: &mut Vec<String>) {
for node in nodes {
if let TemplateNode::Element(e) = node {
for d in &e.directives {
if let Directive::For { var, .. } = d {
out.push(var.clone());
}
}
collect_for_bindings(&e.children, out);
}
}
}
fn codegen_node(code: &mut String, node: &TemplateNode, depth: usize) {
let indent = " ".repeat(depth);
match node {
TemplateNode::Text(s) => {
if !s.trim().is_empty() {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
code.push_str(&format!("{indent}__buf . push_str ( \"{}\" ) ;\n", escaped));
}
}
TemplateNode::Escape { expr } => {
code.push_str(&format!(
"{indent}__buf . push_escaped ( & ({expr}) . to_string () ) ;\n"
));
}
TemplateNode::Raw { expr } => {
code.push_str(&format!(
"{indent}__buf . push_str ( & ({expr}) . to_string () ) ;\n"
));
}
TemplateNode::Element(elem) => codegen_element(code, elem, depth),
}
}
fn codegen_element(code: &mut String, elem: &Element, depth: usize) {
let indent = " ".repeat(depth);
// Determine wrapping control flow from directives
for dir in &elem.directives {
match dir {
Directive::If(cond) => {
code.push_str(&format!("{indent}if {cond} {{\n"));
}
Directive::For { var, iter } => {
code.push_str(&format!("{indent}for {var} in {iter} {{\n"));
}
Directive::Else => {
code.push_str(&format!("{indent}}} else {{\n"));
}
}
}
let inner_depth = if elem.directives.is_empty() { depth } else { depth + 1 };
let inner_indent = " ".repeat(inner_depth);
if elem.is_component {
// Component invocation
let tag = &elem.tag;
let mut prop_args = String::new();
for (k, v) in &elem.attrs {
if !k.starts_with('@') {
prop_args.push_str(&format!("{k} : ({v}) . into () , "));
}
}
code.push_str(&format!(
"{inner_indent}__buf . push_str ( & {tag} :: new ( {prop_args}) . render () . into_string () ) ;\n"
));
} else {
// HTML element: open tag
let open_tag = build_open_tag(elem);
let escaped = open_tag.replace('\\', "\\\\").replace('"', "\\\"");
code.push_str(&format!("{inner_indent}__buf . push_str ( \"{escaped}\" ) ;\n"));
// Children
for child in &elem.children {
codegen_node(code, child, inner_depth);
}
// Close tag
if !elem.self_closing {
let close = format!("</{}>", elem.tag);
code.push_str(&format!("{inner_indent}__buf . push_str ( \"{}\" ) ;\n", close));
}
}
if !elem.directives.is_empty() {
code.push_str(&format!("{indent}}}\n"));
}
}
fn build_open_tag(elem: &Element) -> String {
let mut s = format!("<{}", elem.tag);
for (name, val) in &elem.attrs {
if name.starts_with('@') { continue; }
if val.is_empty() {
s.push_str(&format!(" {name}"));
} else {
s.push_str(&format!(" {name}=\"{val}\""));
}
}
if elem.self_closing {
s.push_str(" />");
} else {
s.push('>');
}
s
}
// ── Helpers ───────────────────────────────────────────────────
fn toks_to_str(toks: &[Tok]) -> String {
toks.iter().map(tok_to_str).collect::<Vec<_>>().join(" ")
}
fn tok_to_str(tok: &Tok) -> String {
match tok {
Tok::Lt => "<".into(),
Tok::Gt => ">".into(),
Tok::Slash => "/".into(),
Tok::At => "@".into(),
Tok::Eq => "=".into(),
Tok::Comma => ",".into(),
Tok::Ident(s) => s.clone(),
Tok::Lit(s) => s.clone(),
Tok::BraceGroup(inner) => format!("{{ {} }}", toks_to_str(inner)),
Tok::Other(s) => s.clone(),
}
}
/// Remove surrounding `"..."` from a string literal token.
fn unquote(s: &str) -> String {
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
s[1..s.len()-1].to_string()
} else {
s.to_string()
}
}
/// Split `"item in &self.items"` → ("item", "&self.items")
fn split_for_expr(s: &str) -> (String, String) {
if let Some(idx) = s.find(" in ") {
let var = s[..idx].trim().to_string();
let iter = s[idx + 4..].trim().to_string();
(var, iter)
} else {
(s.to_string(), String::new())
}
}
fn find_gt(toks: &[Tok], from: usize) -> Option<usize> {
for i in from..toks.len() {
if matches!(toks[i], Tok::Gt) { return Some(i); }
}
None
}
fn reconstruct_tag(toks: &[Tok], start: usize, end: usize) -> String {
toks[start..=end].iter().map(tok_to_str).collect::<Vec<_>>().join("")
}
fn push_text(nodes: &mut Vec<TemplateNode>, text: &str) {
// Normalise whitespace runs to a single space when appending to existing Text
let text = text.replace('\n', " ").replace('\r', "");
if text.trim().is_empty() {
return;
}
match nodes.last_mut() {
Some(TemplateNode::Text(prev)) => prev.push_str(&text),
_ => nodes.push(TemplateNode::Text(text)),
}
}