diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 3263faf6fe5..980749f1fe3 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -632,6 +632,8 @@ pub enum Expression { /// A string literal. The .0 is the content of the string, without the quotes StringLiteral(SmolStr), + /// Include a string from a file using Rust's `include_str!` macro. The path is relative to the .slint file. + IncludeString(SmolStr), /// Number NumberLiteral(f64, Unit), /// Bool @@ -860,6 +862,7 @@ impl Expression { Expression::Invalid => Type::Invalid, Expression::Uncompiled(_) => Type::Invalid, Expression::StringLiteral(_) => Type::String, + Expression::IncludeString(_) => Type::String, Expression::NumberLiteral(_, unit) => unit.ty(), Expression::BoolLiteral(_) => Type::Bool, Expression::PropertyReference(nr) => nr.ty(), @@ -985,6 +988,7 @@ impl Expression { Expression::Invalid => {} Expression::Uncompiled(_) => {} Expression::StringLiteral(_) => {} + Expression::IncludeString(_) => {} Expression::NumberLiteral(_, _) => {} Expression::BoolLiteral(_) => {} Expression::PropertyReference { .. } => {} @@ -1102,6 +1106,7 @@ impl Expression { Expression::Invalid => {} Expression::Uncompiled(_) => {} Expression::StringLiteral(_) => {} + Expression::IncludeString(_) => {} Expression::NumberLiteral(_, _) => {} Expression::BoolLiteral(_) => {} Expression::PropertyReference { .. } => {} @@ -1234,6 +1239,7 @@ impl Expression { Expression::Invalid => true, Expression::Uncompiled(_) => false, Expression::StringLiteral(_) => true, + Expression::IncludeString(_) => true, Expression::NumberLiteral(_, _) => true, Expression::BoolLiteral(_) => true, Expression::PropertyReference(nr) => nr.is_constant(), @@ -1837,6 +1843,7 @@ pub fn pretty_print(f: &mut dyn std::fmt::Write, expression: &Expression) -> std Expression::Invalid => write!(f, ""), Expression::Uncompiled(u) => write!(f, "{u:?}"), Expression::StringLiteral(s) => write!(f, "{s:?}"), + Expression::IncludeString(p) => write!(f, "@include_string({p:?})"), Expression::NumberLiteral(vl, unit) => write!(f, "{vl}{unit}"), Expression::BoolLiteral(b) => write!(f, "{b:?}"), Expression::PropertyReference(a) => write!(f, "{a:?}"), diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 1f8174561ba..34520ba8dc0 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -3318,6 +3318,9 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String use llr::Expression; match expr { Expression::StringLiteral(s) => shared_string_literal(s), + Expression::IncludeString(path) => { + format!("slint::private_api::include_str!({})", shared_string_literal(path)) + } Expression::NumberLiteral(num) => { if !num.is_finite() { // just print something diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index ad83e64edce..b9dca69aebf 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2393,6 +2393,10 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream let s = s.as_str(); quote!(sp::SharedString::from(#s)) } + Expression::IncludeString(path) => { + let path = path.as_str(); + quote!(sp::SharedString::from(include_str!(#path))) + } Expression::KeyboardShortcutLiteral(shortcut) => { let key = &*shortcut.key; let alt = shortcut.modifiers.alt; diff --git a/internal/compiler/llr/expression.rs b/internal/compiler/llr/expression.rs index 66dc5e924a9..3a4cb90094a 100644 --- a/internal/compiler/llr/expression.rs +++ b/internal/compiler/llr/expression.rs @@ -25,6 +25,8 @@ pub enum ArrayOutput { pub enum Expression { /// A string literal. The .0 is the content of the string, without the quotes StringLiteral(SmolStr), + /// Include a string from a file using Rust's `include_str!` macro. The path is relative to the .slint file. + IncludeString(SmolStr), /// Number NumberLiteral(f64), /// Bool @@ -329,6 +331,7 @@ impl Expression { pub fn ty(&self, ctx: &dyn TypeResolutionContext) -> Type { match self { Self::StringLiteral(_) => Type::String, + Self::IncludeString(_) => Type::String, Self::NumberLiteral(_) => Type::Float32, Self::BoolLiteral(_) => Type::Bool, Self::PropertyReference(prop) => ctx.property_ty(prop).clone(), @@ -394,6 +397,7 @@ macro_rules! visit_impl { ($self:ident, $visitor:ident, $as_ref:ident, $iter:ident, $values:ident) => { match $self { Expression::StringLiteral(_) => {} + Expression::IncludeString(_) => {} Expression::NumberLiteral(_) => {} Expression::BoolLiteral(_) => {} Expression::PropertyReference(_) => {} diff --git a/internal/compiler/llr/lower_expression.rs b/internal/compiler/llr/lower_expression.rs index 9942b22cace..29032edb04b 100644 --- a/internal/compiler/llr/lower_expression.rs +++ b/internal/compiler/llr/lower_expression.rs @@ -76,6 +76,7 @@ pub fn lower_expression( } tree_Expression::Uncompiled(_) => panic!(), tree_Expression::StringLiteral(s) => llr_Expression::StringLiteral(s.clone()), + tree_Expression::IncludeString(p) => llr_Expression::IncludeString(p.clone()), tree_Expression::NumberLiteral(n, unit) => { llr_Expression::NumberLiteral(unit.normalize(*n)) } diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index 65302b138f6..97464762320 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -22,6 +22,7 @@ const INLINE_SINGLE_THRESHOLD: isize = ALLOC_COST * 10; fn expression_cost(exp: &Expression, ctx: &EvaluationContext) -> isize { let mut cost = match exp { Expression::StringLiteral(_) => ALLOC_COST, + Expression::IncludeString(_) => ALLOC_COST, Expression::NumberLiteral(_) => 0, Expression::BoolLiteral(_) => 0, Expression::KeyboardShortcutLiteral(_) => 0, diff --git a/internal/compiler/llr/pretty_print.rs b/internal/compiler/llr/pretty_print.rs index c9a66dad32d..bb41166f4e4 100644 --- a/internal/compiler/llr/pretty_print.rs +++ b/internal/compiler/llr/pretty_print.rs @@ -303,6 +303,7 @@ impl<'a, T> Display for DisplayExpression<'a, T> { let e = |e: &'a Expression| DisplayExpression(e, ctx); match self.0 { Expression::StringLiteral(x) => write!(f, "{x:?}"), + Expression::IncludeString(x) => write!(f, "include_str!({x:?})"), Expression::NumberLiteral(x) => write!(f, "{x:?}"), Expression::BoolLiteral(x) => write!(f, "{x:?}"), Expression::KeyboardShortcutLiteral(shortcut) => { diff --git a/internal/compiler/parser.rs b/internal/compiler/parser.rs index ce2bb9ff04e..d778cc08429 100644 --- a/internal/compiler/parser.rs +++ b/internal/compiler/parser.rs @@ -374,12 +374,14 @@ declare_syntax! { // FIXME: the test should test that as alternative rather than several of them (but it can also be a literal) Expression-> [ ?Expression, ?FunctionCallExpression, ?IndexExpression, ?SelfAssignment, ?ConditionalExpression, ?QualifiedName, ?BinaryExpression, ?Array, ?ObjectLiteral, - ?UnaryOpExpression, ?CodeBlock, ?StringTemplate, ?AtImageUrl, ?AtGradient, ?AtTr, + ?UnaryOpExpression, ?CodeBlock, ?StringTemplate, ?AtImageUrl, ?AtIncludeString, ?AtGradient, ?AtTr, ?MemberAccess, ?AtKeys ], /// Concatenate the Expressions to make a string (usually expended from a template string) StringTemplate -> [*Expression], /// `@image-url("foo.png")` AtImageUrl -> [], + /// `@include_string("foo.txt")` + AtIncludeString -> [], /// `@linear-gradient(...)` or `@radial-gradient(...)` AtGradient -> [*Expression], /// `@tr("foo", ...)` // the string is a StringLiteral diff --git a/internal/compiler/parser/expressions.rs b/internal/compiler/parser/expressions.rs index f68ebd0b358..0eaad638145 100644 --- a/internal/compiler/parser/expressions.rs +++ b/internal/compiler/parser/expressions.rs @@ -243,6 +243,9 @@ fn parse_at_keyword(p: &mut impl Parser) { "image-url" | "image_url" => { parse_image_url(p); } + "include-string" | "include_string" => { + parse_include_string(p); + } "linear-gradient" | "linear_gradient" => { parse_gradient(p); } @@ -264,7 +267,7 @@ fn parse_at_keyword(p: &mut impl Parser) { _ => { p.consume(); p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works - p.error("Expected 'image-url', 'tr', 'keys', 'conic-gradient', 'linear-gradient', or 'radial-gradient' after '@'"); + p.error("Expected 'image-url', 'include-string', 'tr', 'keys', 'conic-gradient', 'linear-gradient', or 'radial-gradient' after '@'"); } } } @@ -738,3 +741,37 @@ fn parse_image_url(p: &mut impl Parser) { p.until(SyntaxKind::RParent); } } + +#[cfg_attr(test, parser_test)] +/// ```test,AtIncludeString +/// @include_string("foo.txt") +/// @include_string("foo.txt",) +/// ``` +fn parse_include_string(p: &mut impl Parser) { + let mut p = p.start_node(SyntaxKind::AtIncludeString); + p.consume(); // "@" + p.consume(); // "include-string" + if !(p.expect(SyntaxKind::LParent)) { + return; + } + let peek = p.peek(); + if peek.kind() != SyntaxKind::StringLiteral { + p.error("@include_string must contain a plain path as a string literal"); + p.until(SyntaxKind::RParent); + return; + } + if !peek.as_str().starts_with('"') || !peek.as_str().ends_with('"') { + p.error("@include_string must contain a plain path as a string literal, without any '\\{}' expressions"); + p.until(SyntaxKind::RParent); + return; + } + p.expect(SyntaxKind::StringLiteral); + if !p.test(SyntaxKind::Comma) { + if !p.test(SyntaxKind::RParent) { + p.error("Expected ')' or ','"); + p.until(SyntaxKind::RParent); + } + return; + } + p.expect(SyntaxKind::RParent); +} diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index fff712480d5..9c529f63955 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -366,6 +366,9 @@ impl Expression { NodeOrToken::Node(node) => match node.kind() { SyntaxKind::Expression => Some(Self::from_expression_node(node.into(), ctx)), SyntaxKind::AtImageUrl => Some(Self::from_at_image_url_node(node.into(), ctx)), + SyntaxKind::AtIncludeString => { + Some(Self::from_at_include_string_node(node.into(), ctx)) + } SyntaxKind::AtGradient => Some(Self::from_at_gradient(node.into(), ctx)), SyntaxKind::AtTr => Some(Self::from_at_tr(node.into(), ctx)), SyntaxKind::AtMarkdown => Some(Self::from_at_markdown(node.into(), ctx)), @@ -521,6 +524,43 @@ impl Expression { } } + fn from_at_include_string_node( + node: syntax_nodes::AtIncludeString, + ctx: &mut LookupCtx, + ) -> Self { + let s = match node + .child_text(SyntaxKind::StringLiteral) + .and_then(|x| crate::literals::unescape_string(&x)) + { + Some(s) => s, + None => { + ctx.diag.push_error("Cannot parse string literal".into(), &node); + return Self::Invalid; + } + }; + + if s.is_empty() { + return Expression::StringLiteral(String::new().into()); + } + + let path = std::path::Path::new(&s); + if crate::pathutils::is_absolute(path) { + return Expression::IncludeString(s.into()); + } + + let resolved_path = ctx + .type_loader + .and_then(|loader| loader.resolve_import_path(Some(&(*node).clone().into()), &s)) + .map(|i| i.0.to_string_lossy().into()) + .unwrap_or_else(|| { + crate::pathutils::join(&crate::pathutils::dirname(node.source_file.path()), path) + .map(|p| p.to_string_lossy().into()) + .unwrap_or(s.clone()) + }); + + Expression::IncludeString(resolved_path.into()) + } + pub fn from_at_gradient(node: syntax_nodes::AtGradient, ctx: &mut LookupCtx) -> Self { enum GradKind { Linear { angle: Box }, diff --git a/internal/compiler/passes/resolving/remove_noop.rs b/internal/compiler/passes/resolving/remove_noop.rs index bfb10354fe7..b014d3a3e4c 100644 --- a/internal/compiler/passes/resolving/remove_noop.rs +++ b/internal/compiler/passes/resolving/remove_noop.rs @@ -46,6 +46,7 @@ fn without_side_effects(expression: &Expression) -> bool { } Expression::NumberLiteral(_, _) => true, Expression::StringLiteral(_) => true, + Expression::IncludeString(_) => true, Expression::BoolLiteral(_) => true, Expression::KeyboardShortcut(_) => true, Expression::CodeBlock(expressions) => expressions.iter().all(without_side_effects), diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 0c20ac1a80f..f5835083ad5 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -169,6 +169,11 @@ pub fn eval_expression(expression: &Expression, local_context: &mut EvalLocalCon Expression::Invalid => panic!("invalid expression while evaluating"), Expression::Uncompiled(_) => panic!("uncompiled expression while evaluating"), Expression::StringLiteral(s) => Value::String(s.as_str().into()), + Expression::IncludeString(path) => { + let content = std::fs::read_to_string(path.as_str()) + .unwrap_or_else(|e| panic!("Cannot read file '{}': {}", path, e)); + Value::String(content.into()) + } Expression::NumberLiteral(n, unit) => Value::Number(unit.normalize(*n)), Expression::BoolLiteral(b) => Value::Bool(*b), Expression::ElementReference(_) => todo!(